Import AITURK IDE 1.0.0-beta.1 from Hermes 63279301; preserve MIT license
This commit is contained in:
commit
03634b1ca3
11340 files changed
+3442369
No files matched your search
@@ -0,0 +1,281 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { createClientSessionState } from '@/lib/chat-runtime'
|
||||
import { $gatewayState } from '@/store/session'
|
||||
import { $sessionStates, dropSessionState, publishSessionState } from '@/store/session-states'
|
||||
|
||||
import { host } from './index'
|
||||
|
||||
// Plugins read app state exclusively through host.state — and before this
|
||||
// contract existed, only the PRIMARY workspace tab was reachable
|
||||
// ($activeSessionId). Clicking a tile never moved any plugin-visible atom,
|
||||
// and tile focus is pure renderer state that gateway RPC can never see, so
|
||||
// no plugin-side workaround was possible. These atoms are the plugin door to
|
||||
// the same focused-session signals the core statusbar reads
|
||||
// (use-statusbar-items.tsx).
|
||||
|
||||
describe('host.state focused-session atoms', () => {
|
||||
beforeEach(() => {
|
||||
window.localStorage.clear()
|
||||
vi.resetModules()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.resetModules()
|
||||
})
|
||||
|
||||
async function setup() {
|
||||
const { host } = await import('@/sdk/index')
|
||||
const states = await import('@/store/session-states')
|
||||
const session = await import('@/store/session')
|
||||
|
||||
return { host, states, session }
|
||||
}
|
||||
|
||||
it('exposes readonly atoms for the focused session (runtime id, stored id, usage)', async () => {
|
||||
const { host } = await setup()
|
||||
|
||||
for (const key of ['focusedSessionId', 'focusedStoredSessionId', 'focusedUsage'] as const) {
|
||||
const store = host.state[key]
|
||||
expect(store, key).toBeDefined()
|
||||
expect(typeof store.get, key).toBe('function')
|
||||
expect(typeof store.listen, key).toBe('function')
|
||||
expect(typeof store.subscribe, key).toBe('function')
|
||||
}
|
||||
})
|
||||
|
||||
it('mirrors the primary session while no tile is focused', async () => {
|
||||
const { host, states } = await setup()
|
||||
|
||||
expect(host.state.focusedSessionId.get()).toBe(states.$focusedRuntimeId.get())
|
||||
expect(host.state.focusedStoredSessionId.get()).toBe(states.$focusedStoredSessionId.get())
|
||||
})
|
||||
|
||||
it('focusedUsage projects the focused session usage, null while unresolved', async () => {
|
||||
const { host, states } = await setup()
|
||||
|
||||
const focused = states.$focusedSessionState.get()
|
||||
expect(host.state.focusedUsage.get()).toBe(focused?.usage ?? null)
|
||||
})
|
||||
|
||||
it('exposes the registry source that owns the active gateway', async () => {
|
||||
const { host, session } = await setup()
|
||||
|
||||
session.setConnection({ connectionId: 'work', mode: 'remote' } as never)
|
||||
expect(host.state.connectionId.get()).toBe('work')
|
||||
|
||||
session.setConnection({ mode: 'local' } as never)
|
||||
expect(host.state.connectionId.get()).toBe('local')
|
||||
session.setConnection(null)
|
||||
})
|
||||
|
||||
it('follows the interacted tile while the primary-only atom stays put', async () => {
|
||||
const { host, session, states } = await setup()
|
||||
const tree = await import('@/components/pane-shell/tree/store')
|
||||
const model = await import('@/components/pane-shell/tree/model')
|
||||
const { registry } = await import('@/contrib/registry')
|
||||
|
||||
// A second chat zone holding a session tile, next to the main workspace.
|
||||
for (const id of ['workspace', 'session-tile:tile-a']) {
|
||||
registry.register({
|
||||
area: 'panes',
|
||||
data: id === 'workspace' ? { placement: 'main', uncloseable: true } : { placement: 'main' },
|
||||
id,
|
||||
render: () => null,
|
||||
title: id
|
||||
})
|
||||
}
|
||||
|
||||
tree.declareDefaultTree(
|
||||
model.split('row', [
|
||||
model.group(['workspace'], { active: 'workspace', id: 'grp-main' }),
|
||||
model.group(['session-tile:tile-a'], { active: 'session-tile:tile-a', id: 'grp-side' })
|
||||
])
|
||||
)
|
||||
|
||||
const primaryBefore = session.$activeSessionId.get()
|
||||
const primarySelection = session.$selectedStoredSessionId.get()
|
||||
|
||||
// Bind the tile to a live runtime with its own usage, so the readout
|
||||
// atoms (focusedSessionId / focusedUsage) — not just the navigation id —
|
||||
// are proven to follow the tile.
|
||||
const tileUsage = {
|
||||
calls: 3,
|
||||
input: 1200,
|
||||
output: 300,
|
||||
total: 1500,
|
||||
context_used: 42000,
|
||||
context_max: 200000,
|
||||
context_percent: 21,
|
||||
cost_usd: 0.0123
|
||||
}
|
||||
|
||||
states.$sessionTiles.set([{ storedSessionId: 'tile-a', runtimeId: 'runtime-tile-a' }])
|
||||
states.$sessionStates.set({
|
||||
'runtime-tile-a': { storedSessionId: 'tile-a', usage: tileUsage } as never
|
||||
})
|
||||
|
||||
// Focusing the tile zone moves the focused atoms onto the tile's session…
|
||||
tree.noteActiveTreeGroup('grp-side')
|
||||
expect(host.state.focusedStoredSessionId.get()).toBe('tile-a')
|
||||
expect(host.state.focusedSessionId.get()).toBe('runtime-tile-a')
|
||||
expect(host.state.focusedUsage.get()).toBe(tileUsage)
|
||||
// …while the primary-only atom a plugin used to rely on does not move.
|
||||
expect(host.state.activeSessionId.get()).toBe(primaryBefore)
|
||||
|
||||
// Focusing back homes to the primary's selection, whatever it is.
|
||||
tree.noteActiveTreeGroup('grp-main')
|
||||
expect(host.state.focusedStoredSessionId.get()).toBe(primarySelection)
|
||||
})
|
||||
})
|
||||
|
||||
describe('host.state.focusedSessionProfile', () => {
|
||||
beforeEach(() => {
|
||||
window.localStorage.clear()
|
||||
vi.resetModules()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.resetModules()
|
||||
})
|
||||
|
||||
async function setup() {
|
||||
const { host } = await import('@/sdk/index')
|
||||
const profile = await import('@/store/profile')
|
||||
const session = await import('@/store/session')
|
||||
const states = await import('@/store/session-states')
|
||||
|
||||
return { host, profile, session, states }
|
||||
}
|
||||
|
||||
it('is a readonly atom that falls back to the gateway profile with no focused session', async () => {
|
||||
const { host, profile } = await setup()
|
||||
|
||||
expect(typeof host.state.focusedSessionProfile.get).toBe('function')
|
||||
expect(typeof host.state.focusedSessionProfile.listen).toBe('function')
|
||||
|
||||
profile.$activeGatewayProfile.set('newsanalyst')
|
||||
expect(host.state.focusedSessionProfile.get()).toBe('newsanalyst')
|
||||
profile.$activeGatewayProfile.set('default')
|
||||
})
|
||||
|
||||
it('resolves the focused session to the owner stamped on its session row, not the socket home', async () => {
|
||||
const { host, profile, session } = await setup()
|
||||
|
||||
// The gateway socket is homed on default (e.g. cold start), but the chat
|
||||
// on screen belongs to newsanalyst — the readout must say newsanalyst.
|
||||
profile.$activeGatewayProfile.set('default')
|
||||
session.$sessions.set([{ id: 'news-1', profile: 'newsanalyst' } as never])
|
||||
session.$selectedStoredSessionId.set('news-1')
|
||||
|
||||
expect(host.state.focusedSessionProfile.get()).toBe('newsanalyst')
|
||||
|
||||
// An uncached/unstamped id inherits the gateway profile (the old ladder).
|
||||
session.$selectedStoredSessionId.set('unknown-id')
|
||||
expect(host.state.focusedSessionProfile.get()).toBe('default')
|
||||
|
||||
session.$sessions.set([])
|
||||
session.$selectedStoredSessionId.set(null)
|
||||
})
|
||||
|
||||
it('fails closed when duplicate stored or lineage ids resolve to different connections', async () => {
|
||||
const { host, session } = await setup()
|
||||
|
||||
session.$sessions.set([
|
||||
{
|
||||
connection_id: 'source-a',
|
||||
id: 'shared-tip-a',
|
||||
_lineage_root_id: 'shared-root',
|
||||
profile: 'worker'
|
||||
} as never,
|
||||
{
|
||||
connection_id: 'source-b',
|
||||
id: 'shared-root',
|
||||
profile: 'worker'
|
||||
} as never
|
||||
])
|
||||
session.$selectedStoredSessionId.set('shared-root')
|
||||
|
||||
expect(host.state.focusedSessionOwner.get()).toBeNull()
|
||||
|
||||
session.$sessions.set([])
|
||||
session.$selectedStoredSessionId.set(null)
|
||||
})
|
||||
|
||||
it('fails closed when the same focused id has ambiguous owner hints', async () => {
|
||||
const { host, session } = await setup()
|
||||
|
||||
session.setSessionOwnerHint('ambiguous-hint', {
|
||||
connectionId: 'source-a',
|
||||
mode: 'remote',
|
||||
profile: 'worker',
|
||||
targetProfile: 'backend-worker-a'
|
||||
})
|
||||
session.setSessionOwnerHint('ambiguous-hint', {
|
||||
connectionId: 'source-b',
|
||||
mode: 'remote',
|
||||
profile: 'worker',
|
||||
targetProfile: 'backend-worker-b'
|
||||
})
|
||||
session.$sessions.set([
|
||||
{
|
||||
connection_id: 'source-a',
|
||||
id: 'ambiguous-hint',
|
||||
profile: 'worker'
|
||||
} as never
|
||||
])
|
||||
session.$selectedStoredSessionId.set('ambiguous-hint')
|
||||
|
||||
expect(host.state.focusedSessionOwner.get()).toBeNull()
|
||||
|
||||
session.$sessions.set([])
|
||||
session.$selectedStoredSessionId.set(null)
|
||||
})
|
||||
|
||||
it('keeps the focused session connection when same-named profiles share a handle', async () => {
|
||||
const { host, profile, session } = await setup()
|
||||
|
||||
profile.$activeGatewayProfile.set('default')
|
||||
session.setConnection({ connectionId: 'source-a', mode: 'remote' } as never)
|
||||
session.$sessions.set([
|
||||
{
|
||||
connection_id: 'source-b',
|
||||
id: 'shared-default',
|
||||
profile: 'default'
|
||||
} as never
|
||||
])
|
||||
session.$selectedStoredSessionId.set('shared-default')
|
||||
|
||||
expect(host.state.focusedSessionOwner.get()).toEqual({
|
||||
connectionId: 'source-b',
|
||||
profile: 'default'
|
||||
})
|
||||
|
||||
session.$sessions.set([])
|
||||
session.$selectedStoredSessionId.set(null)
|
||||
session.setConnection(null)
|
||||
})
|
||||
})
|
||||
|
||||
describe('host.state busy vs gateway', () => {
|
||||
afterEach(() => {
|
||||
$sessionStates.set({})
|
||||
$gatewayState.set('idle')
|
||||
})
|
||||
|
||||
it('exposes per-session turn-busy and does not treat gateway as busy', () => {
|
||||
const running = { ...createClientSessionState('stored-a'), busy: true }
|
||||
const idle = { ...createClientSessionState('stored-b'), busy: false }
|
||||
|
||||
publishSessionState('runtime-a', running)
|
||||
publishSessionState('runtime-b', idle)
|
||||
$gatewayState.set('open')
|
||||
|
||||
expect(host.state.busyBySession.get()).toEqual({ 'runtime-a': true, 'runtime-b': false })
|
||||
expect(host.state.gateway.get()).toBe('open')
|
||||
|
||||
dropSessionState('runtime-a')
|
||||
expect(host.state.busyBySession.get()['runtime-a']).toBeUndefined()
|
||||
expect(host.state.gateway.get()).toBe('open')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,233 @@
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
|
||||
import { createClientSessionState } from '@/lib/chat-runtime'
|
||||
import { host } from '@/sdk'
|
||||
import { setActiveSessionId, setAwaitingResponse, setBusy } from '@/store/session'
|
||||
import { clearAllSessionStates, publishSessionState } from '@/store/session-states'
|
||||
|
||||
describe('host.state turn flags', () => {
|
||||
afterEach(() => {
|
||||
setActiveSessionId(null)
|
||||
setBusy(false)
|
||||
setAwaitingResponse(false)
|
||||
clearAllSessionStates()
|
||||
})
|
||||
|
||||
it('uses the draft atoms when there is no runtime session', () => {
|
||||
expect(host.state.busy.get()).toBe(false)
|
||||
expect(host.state.awaitingResponse.get()).toBe(false)
|
||||
|
||||
setBusy(true)
|
||||
setAwaitingResponse(true)
|
||||
|
||||
expect(host.state.busy.get()).toBe(true)
|
||||
expect(host.state.awaitingResponse.get()).toBe(true)
|
||||
})
|
||||
|
||||
it('reads the focused session slice once a runtime exists', () => {
|
||||
setBusy(false)
|
||||
setAwaitingResponse(false)
|
||||
setActiveSessionId('rt-focus')
|
||||
publishSessionState('rt-focus', {
|
||||
...createClientSessionState('stored-focus'),
|
||||
awaitingResponse: true,
|
||||
busy: true
|
||||
})
|
||||
|
||||
expect(host.state.busy.get()).toBe(true)
|
||||
expect(host.state.awaitingResponse.get()).toBe(true)
|
||||
|
||||
publishSessionState('rt-focus', {
|
||||
...createClientSessionState('stored-focus'),
|
||||
awaitingResponse: false,
|
||||
busy: true
|
||||
})
|
||||
|
||||
expect(host.state.busy.get()).toBe(true)
|
||||
expect(host.state.awaitingResponse.get()).toBe(false)
|
||||
})
|
||||
|
||||
it('does not pick up a background session', () => {
|
||||
setActiveSessionId('rt-focus')
|
||||
publishSessionState('rt-focus', createClientSessionState('stored-focus'))
|
||||
publishSessionState('rt-bg', {
|
||||
...createClientSessionState('stored-bg'),
|
||||
awaitingResponse: true,
|
||||
busy: true
|
||||
})
|
||||
|
||||
expect(host.state.busy.get()).toBe(false)
|
||||
expect(host.state.awaitingResponse.get()).toBe(false)
|
||||
})
|
||||
|
||||
it('follows a focused session tile, not the primary', async () => {
|
||||
const tree = await import('@/components/pane-shell/tree/store')
|
||||
const model = await import('@/components/pane-shell/tree/model')
|
||||
const { registry } = await import('@/contrib/registry')
|
||||
const { $sessionTiles } = await import('@/store/session-states')
|
||||
|
||||
// A second chat zone holding a session tile, next to the main workspace.
|
||||
for (const id of ['workspace', 'session-tile:tile-a']) {
|
||||
registry.register({
|
||||
area: 'panes',
|
||||
data: id === 'workspace' ? { placement: 'main', uncloseable: true } : { placement: 'main' },
|
||||
id,
|
||||
render: () => null,
|
||||
title: id
|
||||
})
|
||||
}
|
||||
|
||||
tree.declareDefaultTree(
|
||||
model.split('row', [
|
||||
model.group(['workspace'], { active: 'workspace', id: 'grp-main' }),
|
||||
model.group(['session-tile:tile-a'], { active: 'session-tile:tile-a', id: 'grp-side' })
|
||||
])
|
||||
)
|
||||
|
||||
// Primary chat is idle; the tile's session is mid-turn.
|
||||
setActiveSessionId('rt-primary')
|
||||
publishSessionState('rt-primary', createClientSessionState('stored-primary'))
|
||||
$sessionTiles.set([{ runtimeId: 'rt-tile-a', storedSessionId: 'tile-a' }])
|
||||
publishSessionState('rt-tile-a', {
|
||||
...createClientSessionState('tile-a'),
|
||||
awaitingResponse: true,
|
||||
busy: true
|
||||
})
|
||||
|
||||
// Focusing the tile zone moves the flags onto the tile's session…
|
||||
tree.noteActiveTreeGroup('grp-side')
|
||||
expect(host.state.busy.get()).toBe(true)
|
||||
expect(host.state.awaitingResponse.get()).toBe(true)
|
||||
|
||||
// …and homing back to the workspace returns to the (idle) primary.
|
||||
tree.noteActiveTreeGroup('grp-main')
|
||||
expect(host.state.busy.get()).toBe(false)
|
||||
expect(host.state.awaitingResponse.get()).toBe(false)
|
||||
|
||||
$sessionTiles.set([])
|
||||
})
|
||||
})
|
||||
|
||||
describe('host.connections', () => {
|
||||
const desktopWindow = window as unknown as { hermesDesktop?: Window['hermesDesktop'] }
|
||||
const originalDesktop = desktopWindow.hermesDesktop
|
||||
|
||||
const connection = (id: string, label: string) => ({
|
||||
id,
|
||||
kind: 'remote' as const,
|
||||
label,
|
||||
tokenPreview: null,
|
||||
tokenSet: true,
|
||||
url: `https://${id}.example`
|
||||
})
|
||||
|
||||
const stubBridge = (list: () => Promise<unknown>) => {
|
||||
desktopWindow.hermesDesktop = {
|
||||
...originalDesktop,
|
||||
connections: { list }
|
||||
} as unknown as Window['hermesDesktop']
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
desktopWindow.hermesDesktop = originalDesktop
|
||||
})
|
||||
|
||||
it('returns the registry rows, not the envelope that carries them (#89823)', async () => {
|
||||
stubBridge(async () => ({
|
||||
connections: [connection('local', 'This Mac'), connection('homelab', 'Homelab')],
|
||||
primary: 'local',
|
||||
secureTokenStorage: true,
|
||||
version: 2
|
||||
}))
|
||||
|
||||
const connections = await host.connections()
|
||||
|
||||
expect(Array.isArray(connections)).toBe(true)
|
||||
expect(connections.map(entry => entry.id)).toEqual(['local', 'homelab'])
|
||||
expect(connections[1]).toMatchObject({ kind: 'remote', label: 'Homelab', url: 'https://homelab.example' })
|
||||
})
|
||||
|
||||
it('folds the envelope-level primary id down onto the row that owns it', async () => {
|
||||
stubBridge(async () => ({
|
||||
connections: [connection('local', 'This Mac'), connection('homelab', 'Homelab')],
|
||||
primary: 'homelab',
|
||||
secureTokenStorage: true,
|
||||
version: 2
|
||||
}))
|
||||
|
||||
expect((await host.connections()).map(entry => [entry.id, entry.primary])).toEqual([
|
||||
['local', false],
|
||||
['homelab', true]
|
||||
])
|
||||
})
|
||||
|
||||
it('reads as a single-source desktop when the payload carries no rows', async () => {
|
||||
stubBridge(async () => ({ primary: '', secureTokenStorage: true, version: 1 }))
|
||||
|
||||
await expect(host.connections()).resolves.toEqual([])
|
||||
})
|
||||
|
||||
it('still rejects on a Desktop build without the connection registry', async () => {
|
||||
desktopWindow.hermesDesktop = undefined
|
||||
|
||||
await expect(host.connections()).rejects.toThrow('This Desktop build has no connection registry')
|
||||
})
|
||||
})
|
||||
|
||||
describe('host workspace scope', () => {
|
||||
afterEach(async () => {
|
||||
host.setWorkspaceScope('sessions')
|
||||
const tree = await import('@/components/pane-shell/tree/store')
|
||||
tree.$newSessionTabAction.set(null)
|
||||
tree.removeTreePane('plugin-workspace:scope-test')
|
||||
})
|
||||
|
||||
it('registers plugin workspace chrome options', async () => {
|
||||
const { registry } = await import('@/contrib/registry')
|
||||
|
||||
const close = host.openWorkspace('scope-test', {
|
||||
dock: { pane: 'workspace', pos: 'right' },
|
||||
headerVeto: true,
|
||||
render: () => null,
|
||||
title: 'Scoped',
|
||||
uncloseable: true
|
||||
})
|
||||
|
||||
expect(registry.getArea('panes').find(pane => pane.id === 'plugin-workspace:scope-test')).toMatchObject({
|
||||
data: {
|
||||
dock: { pane: 'workspace', pos: 'right' },
|
||||
headerVeto: true,
|
||||
uncloseable: true
|
||||
}
|
||||
})
|
||||
|
||||
close()
|
||||
})
|
||||
|
||||
it('publishes the active workspace scope through one host seam', async () => {
|
||||
const { $workspaceMode, $workspaceOwnerKey } = await import('@/components/pane-shell/workspace-scope')
|
||||
|
||||
expect(host.setWorkspaceScope('bots', 'connection-b::default')).toBe(true)
|
||||
expect($workspaceMode.get()).toBe('bots')
|
||||
expect($workspaceOwnerKey.get()).toBe('connection-b::default')
|
||||
})
|
||||
|
||||
it('uses the shared tab action for an exact Bot owner without moving Sessions', async () => {
|
||||
const tree = await import('@/components/pane-shell/tree/store')
|
||||
const { $workspaceNewSessionTarget } = await import('@/components/pane-shell/workspace-scope')
|
||||
const opened: string[] = []
|
||||
|
||||
const route = {
|
||||
connectionId: 'connection-b',
|
||||
mode: 'remote' as const,
|
||||
profile: 'writer',
|
||||
targetProfile: 'writer'
|
||||
}
|
||||
|
||||
tree.$newSessionTabAction.set(() => opened.push('tab'))
|
||||
host.newChat(route, { workspaceMode: 'bots', workspaceOwnerKey: 'bot:connection-b::writer' })
|
||||
|
||||
expect(opened).toEqual(['tab'])
|
||||
expect($workspaceNewSessionTarget.get()).toEqual({ kind: 'route', route })
|
||||
})
|
||||
})
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,69 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { planPluginOpenSession } from './plugin-open-session-plan'
|
||||
|
||||
describe('planPluginOpenSession', () => {
|
||||
it('defaults to navigation: dial the worker, do not steal chrome', () => {
|
||||
const plan = planPluginOpenSession({
|
||||
activeProfile: 'default',
|
||||
profile: 'worker'
|
||||
})
|
||||
|
||||
expect(plan.switchWorkspace).toBeNull()
|
||||
expect(plan.dialWithoutSwitching).toBe('worker')
|
||||
expect(plan.showAllProfiles).toBe(true)
|
||||
expect(plan.requireActiveProfileForHydration).toBe(false)
|
||||
})
|
||||
|
||||
it('does not steal chrome even when keepAllProfilesScope is explicitly true', () => {
|
||||
const plan = planPluginOpenSession({
|
||||
activeProfile: 'default',
|
||||
keepAllProfilesScope: true,
|
||||
profile: 'worker'
|
||||
})
|
||||
|
||||
expect(plan.switchWorkspace).toBeNull()
|
||||
expect(plan.dialWithoutSwitching).toBe('worker')
|
||||
expect(plan.showAllProfiles).toBe(true)
|
||||
expect(plan.requireActiveProfileForHydration).toBe(false)
|
||||
})
|
||||
|
||||
it('does not re-dial when chrome is already on that profile', () => {
|
||||
const plan = planPluginOpenSession({
|
||||
activeProfile: 'worker',
|
||||
keepAllProfilesScope: true,
|
||||
profile: 'worker'
|
||||
})
|
||||
|
||||
expect(plan.switchWorkspace).toBeNull()
|
||||
expect(plan.dialWithoutSwitching).toBeNull()
|
||||
// Still restores the unified list: re-opening an already-live bot must not
|
||||
// leave Sessions collapsed onto a profile whose forever-chat is hidden.
|
||||
expect(plan.showAllProfiles).toBe(true)
|
||||
})
|
||||
|
||||
it('treats keepAllProfilesScope:false as an explicit workspace switch', () => {
|
||||
const plan = planPluginOpenSession({
|
||||
activeProfile: 'default',
|
||||
keepAllProfilesScope: false,
|
||||
profile: 'worker'
|
||||
})
|
||||
|
||||
expect(plan.switchWorkspace).toBe('worker')
|
||||
expect(plan.dialWithoutSwitching).toBeNull()
|
||||
expect(plan.showAllProfiles).toBe(false)
|
||||
expect(plan.requireActiveProfileForHydration).toBe(true)
|
||||
})
|
||||
|
||||
it('treats an empty profile as neither a dial nor a workspace switch', () => {
|
||||
const plan = planPluginOpenSession({
|
||||
activeProfile: 'default',
|
||||
keepAllProfilesScope: true,
|
||||
profile: ' '
|
||||
})
|
||||
|
||||
expect(plan.switchWorkspace).toBeNull()
|
||||
expect(plan.dialWithoutSwitching).toBeNull()
|
||||
expect(plan.showAllProfiles).toBeNull()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,50 @@
|
||||
/** How `host.openSession` should treat a named profile.
|
||||
|
||||
A plugin/Bot Mode open is navigation, not a workspace or chrome API-home
|
||||
switch. `keepAllProfilesScope` defaults true (undefined counts as true).
|
||||
Pass false for an explicit profile workspace switch. */
|
||||
export interface PluginOpenSessionPlan {
|
||||
/** Dial this profile's backend without making it chrome-home. */
|
||||
dialWithoutSwitching: null | string
|
||||
/** Hydration may wait until `$activeGatewayProfile` matches the target. */
|
||||
requireActiveProfileForHydration: boolean
|
||||
/** Unified Sessions list vs collapse onto the switched profile. Null = leave. */
|
||||
showAllProfiles: boolean | null
|
||||
/** Activate this profile as the workspace/API home. Null = do not steal chrome. */
|
||||
switchWorkspace: null | string
|
||||
}
|
||||
|
||||
export function planPluginOpenSession(input: {
|
||||
activeProfile: string
|
||||
keepAllProfilesScope?: boolean
|
||||
profile: string
|
||||
}): PluginOpenSessionPlan {
|
||||
const profile = (input.profile ?? '').trim()
|
||||
const activeProfile = (input.activeProfile ?? '').trim()
|
||||
const keepWorkspace = input.keepAllProfilesScope !== false
|
||||
|
||||
if (!profile) {
|
||||
return {
|
||||
dialWithoutSwitching: null,
|
||||
requireActiveProfileForHydration: false,
|
||||
showAllProfiles: null,
|
||||
switchWorkspace: null
|
||||
}
|
||||
}
|
||||
|
||||
if (keepWorkspace) {
|
||||
return {
|
||||
dialWithoutSwitching: profile !== activeProfile ? profile : null,
|
||||
requireActiveProfileForHydration: false,
|
||||
showAllProfiles: true,
|
||||
switchWorkspace: null
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
dialWithoutSwitching: null,
|
||||
requireActiveProfileForHydration: true,
|
||||
showAllProfiles: false,
|
||||
switchWorkspace: profile
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* Runtime SDK injection — the other half of the vscode-module model. Bundled
|
||||
* plugins resolve `@hermes/plugin-sdk` through the vite alias; RUNTIME-loaded
|
||||
* plugins (disk / fetched) import the same specifier and get the same object:
|
||||
* the loader rewrites bare specifiers to shim modules that re-export the
|
||||
* live namespaces installed here. React ships as the app's singletons —
|
||||
* a second React instance would break hooks.
|
||||
*/
|
||||
|
||||
import * as React from 'react'
|
||||
import * as jsxDevRuntime from 'react/jsx-dev-runtime'
|
||||
import * as jsxRuntime from 'react/jsx-runtime'
|
||||
|
||||
import * as sdk from './index'
|
||||
|
||||
const GLOBALS = {
|
||||
__HERMES_PLUGIN_SDK__: sdk,
|
||||
__HERMES_REACT__: React,
|
||||
__HERMES_REACT_JSX__: jsxRuntime,
|
||||
__HERMES_REACT_JSX_DEV__: jsxDevRuntime
|
||||
} as const
|
||||
|
||||
export function installPluginSdk(): void {
|
||||
Object.assign(globalThis, GLOBALS)
|
||||
}
|
||||
|
||||
/** Build a shim ESM blob that re-exports a global namespace's live members.
|
||||
* Export names come from the namespace itself, so the list can't drift. */
|
||||
function shimUrl(globalKey: keyof typeof GLOBALS): string {
|
||||
const names = Object.keys(GLOBALS[globalKey]).filter(name => name !== 'default' && /^[A-Za-z_$][\w$]*$/.test(name))
|
||||
|
||||
const source =
|
||||
`const m = globalThis.${globalKey};\n` +
|
||||
`export default m.default ?? m;\n` +
|
||||
// Guard the destructuring: `export const { } = m` is a syntax error, so
|
||||
// only emit it when the namespace actually has named exports.
|
||||
(names.length ? `export const { ${names.join(', ')} } = m;\n` : '')
|
||||
|
||||
return URL.createObjectURL(new Blob([source], { type: 'text/javascript' }))
|
||||
}
|
||||
|
||||
let cached: Record<string, string> | null = null
|
||||
|
||||
/** Specifier -> shim URL map for the runtime loader (longest keys first). */
|
||||
export function sdkImportMap(): Record<string, string> {
|
||||
cached ??= {
|
||||
'@hermes/plugin-sdk': shimUrl('__HERMES_PLUGIN_SDK__'),
|
||||
'react/jsx-dev-runtime': shimUrl('__HERMES_REACT_JSX_DEV__'),
|
||||
'react/jsx-runtime': shimUrl('__HERMES_REACT_JSX__'),
|
||||
react: shimUrl('__HERMES_REACT__')
|
||||
}
|
||||
|
||||
return cached
|
||||
}
|
||||
Reference in new issue
Block a user