Skip to content

feat(Workspaces): added connecting to workspace using kortex-cli terminal#1222

Open
gastoner wants to merge 1 commit intokortex-hub:mainfrom
gastoner:connecting_to_workspace
Open

feat(Workspaces): added connecting to workspace using kortex-cli terminal#1222
gastoner wants to merge 1 commit intokortex-hub:mainfrom
gastoner:connecting_to_workspace

Conversation

@gastoner
Copy link
Copy Markdown
Contributor

@gastoner gastoner commented Apr 2, 2026

This PR adds support for opening the running workspace terminal in Kortex

To test this, create a workspace using cli
kortex-cli init --runtime podman --agent claude
Run kortex open the workspace terminal, start the workspace (you have to do it manually even if the workspace is actually running from CLI) - kortex is not detecting if the workspace is running or not...

Closes #1125

Screen.Recording.2026-04-02.at.7.55.04.mov

@gastoner gastoner requested a review from a team as a code owner April 2, 2026 05:58
@gastoner gastoner requested review from bmahabirbu and fbricon and removed request for a team April 2, 2026 05:58
@codecov
Copy link
Copy Markdown

codecov bot commented Apr 2, 2026

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 2, 2026

📝 Walkthrough

Walkthrough

Adds terminal support for agent workspaces: main process spawns per-workspace CLI terminals, exposes IPC handlers for terminal lifecycle and I/O, preload bridges new terminal APIs to renderer, and renderer introduces a Svelte terminal component with persistent terminal state.

Changes

Cohort / File(s) Summary
Main Process Manager & Tests
packages/main/src/plugin/agent-workspace/agent-workspace-manager.ts, packages/main/src/plugin/agent-workspace/agent-workspace-manager.spec.ts
Switched workspace config loading to read workspace.json (fallback to { name } on ENOENT); added shellInAgentWorkspace() to spawn kortex-cli terminal ...; added IPC handlers agent-workspace:terminal, agent-workspace:terminalSend, agent-workspace:terminalResize; dispose() now kills tracked processes; tests updated to mock spawn and webContents.
Preload Bridge
packages/preload/src/index.ts
Added contextBridge APIs: shellInAgentWorkspace, shellInAgentWorkspaceSend, shellInAgentWorkspaceResize; registered ipcRenderer.on handlers for agent-workspace:terminal-onData, -onError, -onEnd and callback routing.
Renderer — Details & Tab
packages/renderer/src/lib/agent-workspaces/AgentWorkspaceDetails.svelte, packages/renderer/src/lib/agent-workspaces/AgentWorkspaceDetails.spec.ts
Added "Terminal" tab and route to workspace details and a test asserting the Terminal tab is rendered.
Renderer — Terminal Component & Tests
packages/renderer/src/lib/agent-workspaces/AgentWorkspaceTerminal.svelte, packages/renderer/src/lib/agent-workspaces/AgentWorkspaceTerminal.spec.ts
New AgentWorkspaceTerminal Svelte component integrating xterm.js: loads config, restores serialized content, wires shell I/O via window.shellInAgentWorkspace, handles resize and lifecycle; tests cover status gating, shell invocation, data rendering, and unmount serialization.
Renderer — Terminal Store & Tests
packages/renderer/src/stores/agent-workspace-terminal-store.ts, packages/renderer/src/stores/agent-workspace-terminal-store.spec.ts
Added agentWorkspaceTerminals writable store, registerTerminal() and getExistingTerminal() functions, and logic to remove terminals when workspaces are deleted; tests cover registration, overwrite, retrieval, and cleanup.

Sequence Diagram(s)

sequenceDiagram
    participant Renderer as Renderer (AgentWorkspaceTerminal)
    participant Preload as Preload Bridge
    participant Main as Main Process (AgentWorkspaceManager)
    participant CLI as CLI Process (kortex-cli)

    Renderer->>Preload: shellInAgentWorkspace(id, onData,onError,onEnd)
    Preload->>Main: ipcInvoke('agent-workspace:terminal', id, callbackId)
    Main->>CLI: spawn('kortex-cli', ['terminal', id, '--', '/bin/sh', '-c', '...'])
    Main->>Main: store callbacks & process by callbackId
    Main-->>Preload: return callbackId
    Preload-->>Renderer: return callbackId

    CLI->>Main: stdout/stderr data
    Main->>Preload: webContents.send('agent-workspace:terminal-onData', {callbackId, data})
    Preload->>Renderer: dispatch onData(callbackId, data)
    Renderer->>Renderer: update xterm display

    Renderer->>Preload: shellInAgentWorkspaceSend(callbackId, input)
    Preload->>Main: ipcInvoke('agent-workspace:terminalSend', callbackId, input)
    Main->>CLI: process.stdin.write(input)

    Renderer->>Preload: shellInAgentWorkspaceResize(callbackId, w,h)
    Preload->>Main: ipcInvoke('agent-workspace:terminalResize', callbackId, w, h)

    CLI->>Main: process close
    Main->>Preload: webContents.send('agent-workspace:terminal-onEnd', {callbackId})
    Preload->>Renderer: dispatch onEnd(callbackId)
    Renderer->>Renderer: cleanup & optionally re-register session
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The PR title clearly and specifically describes the main feature: adding terminal connectivity to workspaces using kortex-cli, which aligns with the changeset.
Description check ✅ Passed The PR description is related to the changeset, explaining the feature, testing instructions, and linking to the issue being closed.
Linked Issues check ✅ Passed The PR implements the core requirement from issue #1125: enabling terminal connection to running agent workspaces through the workspace detail page.
Out of Scope Changes check ✅ Passed All changes are focused on implementing terminal connectivity for agent workspaces; no unrelated modifications were detected.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

🧹 Nitpick comments (2)
packages/main/src/plugin/util/exec.ts (1)

77-89: Extract the Flatpak wrapping into a shared helper.

createProcess() now duplicates the command/args rewrite that exec() already does a few lines below, and the two branches are already diverging (exec() forwards extra env with --env=..., while createProcess() cannot). Pulling that logic into one helper will keep the terminal path and the regular exec path from drifting.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/main/src/plugin/util/exec.ts` around lines 77 - 89, create a single
shared helper (e.g., wrapForFlatpak or prepareCommandForFlatpak) that
encapsulates the Flatpak rewrite logic currently duplicated between
createProcess() and exec(): accept the original command, args and env, return
the finalCommand, finalArgs and adjusted env; move the '--host' wrapping and
also the env forwarding behavior (injecting '--env=KEY=VALUE' entries that
exec() uses) into this helper so both createProcess() and exec() call it and
then spawn/exec the returned finalCommand/finalArgs with the returned env;
update references in createProcess() and exec() to use the helper and remove
duplicated rewrite code so terminal and regular exec paths stay consistent.
packages/main/src/plugin/agent-workspace/agent-workspace-manager.spec.ts (1)

358-375: Assert kill() in the dispose() test.

This currently passes even if dispose() stops terminating tracked terminals, so the new cleanup path is effectively untested.

✅ Tighten the assertion
     await manager.shellInAgentWorkspace('ws-1', vi.fn(), vi.fn(), vi.fn());

     manager.dispose();

-    expect(true).toBe(true);
+    expect(mockProcess.kill).toHaveBeenCalledTimes(1);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/main/src/plugin/agent-workspace/agent-workspace-manager.spec.ts`
around lines 358 - 375, The test for dispose() doesn't assert that tracked
terminal processes are terminated; update the test that uses
shellInAgentWorkspace and the mocked process (returned by exec.createProcess) to
assert that mockProcess.kill was called when manager.dispose() is invoked (e.g.,
replace the placeholder expect(true).toBe(true) with an assertion like
expect(mockProcess.kill).toHaveBeenCalled()), ensuring the mocked
ChildProcessWithoutNullStreams instance created in the test is the same one
tracked and killed by dispose().
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/main/src/plugin/agent-workspace/agent-workspace-manager.spec.ts`:
- Around line 256-260: The mock parameter types in createMockProcess are using
the factory type `typeof vi.fn` instead of the mock instance type; update the
parameter types for stdoutOn, stderrOn, and processOn to `ReturnType<typeof
vi.fn>` and also change any casts that use `as typeof vi.fn` (the casts at the
spots corresponding to lines with the mocks referenced in the diff: the casts
around the stdoutOn, stderrOn, and processOn usages) to `as ReturnType<typeof
vi.fn>` so the function signature and all mock usages (createMockProcess and its
mocked stdoutOn/stderrOn/processOn instances) use the correct mock instance
type.

In `@packages/main/src/plugin/agent-workspace/agent-workspace-manager.ts`:
- Around line 174-215: Add an IPC handler to close individual terminal sessions:
implement a new ipcHandle (e.g. 'agent-workspace:terminalClose') that takes
onDataId, looks up this.terminalProcesses and this.terminalCallbacks, calls an
orderly shutdown on the stored process (invocation.process.kill()/terminate or
invocation.process.close() depending on how shellInAgentWorkspace exposes it),
removes entries from this.terminalCallbacks and this.terminalProcesses, and
emits the same 'agent-workspace:terminal-onEnd' notification to the renderer;
update shellInAgentWorkspace usage sites if needed to expose a safe close
method. Also call this new IPC from AgentWorkspaceTerminal.svelte in onDestroy
before clearing sendCallbackId so the child process is not leaked.

In `@packages/preload/src/index.ts`:
- Around line 346-394: The bridge currently registers global ipcRenderer
handlers and only deletes entries from onDataCallbacksShellInAgentWorkspace on
'agent-workspace:terminal-onEnd', which leaks closures if sessions fail or are
rebound; add an explicit dispose/unbind API and per-callback listener cleanup:
expose a new method (e.g. 'shellInAgentWorkspaceDispose' or
'shellInAgentWorkspaceUnbind') that takes the callbackId, calls ipcInvoke to
tell the main process to close the terminal session, deletes the entry from
onDataCallbacksShellInAgentWorkspace, and removes any per-callback ipcRenderer
listeners; to support removal, change the global ipcRenderer.on usage for
'agent-workspace:terminal-onData' / '-onError' / '-onEnd' to either store and
register per-callback bound listener functions (so you can call
ipcRenderer.removeListener with those functions when disposing) or wrap handlers
to no-op when the callbackId is missing; update usages tied to
onDataCallbacksShellInAgentWorkspaceId, onDataCallbacksShellInAgentWorkspace,
and the exposed 'shellInAgentWorkspace' so the new dispose/unbind flow is
invoked on unmount or on invoke failure.

In `@packages/renderer/src/lib/agent-workspaces/AgentWorkspaceTerminal.spec.ts`:
- Around line 57-80: AgentWorkspaceTerminal.svelte currently derives isRunning
from agentWorkspaceStatuses.get(workspaceId) and early-returns before calling
shellInAgentWorkspace, which blocks manual/external starts; change the logic in
AgentWorkspaceTerminal.svelte to not exclusively gate shell initialization on
agentWorkspaceStatuses.get — add a fallback health check or external-status sync
(e.g., call a new checkWorkspaceRunning(workspaceId) or subscribe to an external
event) when agentWorkspaceStatuses.get(...) === 'stopped' and only return after
that check confirms stopped; update the initialization flow around isRunning and
the shellInAgentWorkspace call so shellInAgentWorkspace(workspaceId, ...) is
invoked if the health check or synced status indicates running, and ensure the
code references/updates agentWorkspaceStatuses when the external status is
discovered.

In `@packages/renderer/src/lib/agent-workspaces/AgentWorkspaceTerminal.svelte`:
- Around line 34-40: The startup race and duplicate input-forwarding happen
because executeShellInWorkspace() runs concurrently with refreshTerminal() and
each start/reconnect adds another input handler; fix by serializing session
startup (make executeShellInWorkspace await refreshTerminal() before reading
shellTerminal.cols/rows and set a "starting" mutex/flag so concurrent calls
queue or return) and ensure a single input forwarder by tracking and removing
the previous handler or guarding with an attached boolean (e.g.,
inputForwarderAttached) in the code paths that add the input-forwarding listener
inside executeShellInWorkspace/refreshTerminal/shellTerminal so you only add the
listener once and remove or reuse it on reconnect.
- Around line 121-130: The resize listener is added anonymously which prevents
removal on component teardown; change the anonymous arrow to a named handler
(e.g., const onWindowResize = () => { ... }) that calls fitAddon.fit() and
invokes window.shellInAgentWorkspaceResize(sendCallbackId, shellTerminal.cols,
shellTerminal.rows) as before, register it with
window.addEventListener('resize', onWindowResize) and then remove it in the
component cleanup (onDestroy or equivalent) via
window.removeEventListener('resize', onWindowResize); reference symbols:
fitAddon, sendCallbackId, shellTerminal, currentRouterPath, workspaceId, and
window.shellInAgentWorkspaceResize when making the change.

In `@packages/renderer/src/stores/agent-workspace-terminal-store.ts`:
- Around line 53-58: registerTerminal mutates the updater parameter and
reassigns it, which violates the noParameterAssign rule and mixes mutation with
reassignment; change the update to return a fresh array instead of mutating
`list` (e.g., use the result of filter combined with the new terminal via array
spread or concat) so `agentWorkspaceTerminals.update` receives an immutable new
array containing all filtered entries plus the `terminal`.

---

Nitpick comments:
In `@packages/main/src/plugin/agent-workspace/agent-workspace-manager.spec.ts`:
- Around line 358-375: The test for dispose() doesn't assert that tracked
terminal processes are terminated; update the test that uses
shellInAgentWorkspace and the mocked process (returned by exec.createProcess) to
assert that mockProcess.kill was called when manager.dispose() is invoked (e.g.,
replace the placeholder expect(true).toBe(true) with an assertion like
expect(mockProcess.kill).toHaveBeenCalled()), ensuring the mocked
ChildProcessWithoutNullStreams instance created in the test is the same one
tracked and killed by dispose().

In `@packages/main/src/plugin/util/exec.ts`:
- Around line 77-89: create a single shared helper (e.g., wrapForFlatpak or
prepareCommandForFlatpak) that encapsulates the Flatpak rewrite logic currently
duplicated between createProcess() and exec(): accept the original command, args
and env, return the finalCommand, finalArgs and adjusted env; move the '--host'
wrapping and also the env forwarding behavior (injecting '--env=KEY=VALUE'
entries that exec() uses) into this helper so both createProcess() and exec()
call it and then spawn/exec the returned finalCommand/finalArgs with the
returned env; update references in createProcess() and exec() to use the helper
and remove duplicated rewrite code so terminal and regular exec paths stay
consistent.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 596e2335-61e5-492a-837a-66faa5765fa4

📥 Commits

Reviewing files that changed from the base of the PR and between 1c59e7b and 76ce7ff.

📒 Files selected for processing (11)
  • packages/main/src/plugin/agent-workspace/agent-workspace-manager.spec.ts
  • packages/main/src/plugin/agent-workspace/agent-workspace-manager.ts
  • packages/main/src/plugin/util/exec.spec.ts
  • packages/main/src/plugin/util/exec.ts
  • packages/preload/src/index.ts
  • packages/renderer/src/lib/agent-workspaces/AgentWorkspaceDetails.spec.ts
  • packages/renderer/src/lib/agent-workspaces/AgentWorkspaceDetails.svelte
  • packages/renderer/src/lib/agent-workspaces/AgentWorkspaceTerminal.spec.ts
  • packages/renderer/src/lib/agent-workspaces/AgentWorkspaceTerminal.svelte
  • packages/renderer/src/stores/agent-workspace-terminal-store.spec.ts
  • packages/renderer/src/stores/agent-workspace-terminal-store.ts

Comment on lines +174 to +215
this.ipcHandle(
'agent-workspace:terminal',
async (_listener: unknown, id: string, onDataId: number): Promise<number> => {
const invocation = await this.shellInAgentWorkspace(
id,
(content: string) => {
this.webContents.send('agent-workspace:terminal-onData', onDataId, content);
},
(error: string) => {
this.webContents.send('agent-workspace:terminal-onError', onDataId, error);
},
() => {
this.webContents.send('agent-workspace:terminal-onEnd', onDataId);
this.terminalCallbacks.delete(onDataId);
this.terminalProcesses.delete(onDataId);
},
);
this.terminalCallbacks.set(onDataId, { write: invocation.write, resize: invocation.resize });
this.terminalProcesses.set(onDataId, invocation.process);
return onDataId;
},
);

this.ipcHandle(
'agent-workspace:terminalSend',
async (_listener: unknown, onDataId: number, content: string): Promise<void> => {
const callback = this.terminalCallbacks.get(onDataId);
if (callback) {
callback.write(content);
}
},
);

this.ipcHandle(
'agent-workspace:terminalResize',
async (_listener: unknown, onDataId: number, width: number, height: number): Promise<void> => {
const callback = this.terminalCallbacks.get(onDataId);
if (callback) {
callback.resize(width, height);
}
},
);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Expose a close IPC for individual terminal sessions.

Lines 174-215 start and retain kortex-cli terminal children, but the only cleanup paths are the process exiting on its own and the manager-wide dispose() in Lines 218-227. AgentWorkspaceTerminal.svelte only disposes the local xterm instance on destroy, so leaving the terminal tab leaks the child process and reopening the tab starts another shell.

🧹 Suggested handler
     this.ipcHandle(
       'agent-workspace:terminalResize',
       async (_listener: unknown, onDataId: number, width: number, height: number): Promise<void> => {
         const callback = this.terminalCallbacks.get(onDataId);
         if (callback) {
           callback.resize(width, height);
         }
       },
     );
+
+    this.ipcHandle(
+      'agent-workspace:terminalClose',
+      async (_listener: unknown, onDataId: number): Promise<void> => {
+        const proc = this.terminalProcesses.get(onDataId);
+        if (proc && !proc.killed) {
+          proc.kill();
+        }
+        this.terminalProcesses.delete(onDataId);
+        this.terminalCallbacks.delete(onDataId);
+      },
+    );
   }

Please pair this with a renderer-side call from AgentWorkspaceTerminal.svelte's onDestroy before clearing sendCallbackId.

Also applies to: 218-227

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/main/src/plugin/agent-workspace/agent-workspace-manager.ts` around
lines 174 - 215, Add an IPC handler to close individual terminal sessions:
implement a new ipcHandle (e.g. 'agent-workspace:terminalClose') that takes
onDataId, looks up this.terminalProcesses and this.terminalCallbacks, calls an
orderly shutdown on the stored process (invocation.process.kill()/terminate or
invocation.process.close() depending on how shellInAgentWorkspace exposes it),
removes entries from this.terminalCallbacks and this.terminalProcesses, and
emits the same 'agent-workspace:terminal-onEnd' notification to the renderer;
update shellInAgentWorkspace usage sites if needed to expose a safe close
method. Also call this new IPC from AgentWorkspaceTerminal.svelte in onDestroy
before clearing sendCallbackId so the child process is not leaked.

Comment on lines +346 to +394
contextBridge.exposeInMainWorld(
'shellInAgentWorkspace',
async (
id: string,
onData: (data: string) => void,
onError: (error: string) => void,
onEnd: () => void,
): Promise<number> => {
onDataCallbacksShellInAgentWorkspaceId++;
onDataCallbacksShellInAgentWorkspace.set(onDataCallbacksShellInAgentWorkspaceId, { onData, onError, onEnd });
return ipcInvoke('agent-workspace:terminal', id, onDataCallbacksShellInAgentWorkspaceId);
},
);

contextBridge.exposeInMainWorld(
'shellInAgentWorkspaceSend',
async (dataId: number, content: string): Promise<void> => {
return ipcInvoke('agent-workspace:terminalSend', dataId, content);
},
);

contextBridge.exposeInMainWorld(
'shellInAgentWorkspaceResize',
async (dataId: number, width: number, height: number): Promise<void> => {
return ipcInvoke('agent-workspace:terminalResize', dataId, width, height);
},
);

ipcRenderer.on('agent-workspace:terminal-onData', (_, callbackId: number, data: string) => {
const callback = onDataCallbacksShellInAgentWorkspace.get(callbackId);
if (callback) {
callback.onData(data);
}
});

ipcRenderer.on('agent-workspace:terminal-onError', (_, callbackId: number, error: string) => {
const callback = onDataCallbacksShellInAgentWorkspace.get(callbackId);
if (callback) {
callback.onError(error);
}
});

ipcRenderer.on('agent-workspace:terminal-onEnd', (_, callbackId: number) => {
const callback = onDataCallbacksShellInAgentWorkspace.get(callbackId);
if (callback) {
callback.onEnd();
onDataCallbacksShellInAgentWorkspace.delete(callbackId);
}
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Allow terminal sessions to be rebound or explicitly disposed.

The renderer now persists callbackId per workspace in packages/renderer/src/stores/agent-workspace-terminal-store.ts, but this bridge only ever registers handlers once and only removes them on agent-workspace:terminal-onEnd. If the terminal view unmounts or the initial ipcInvoke('agent-workspace:terminal', ...) fails, those closures stay live and the next mount has no way to replace or close that session. That can leak handlers and lose terminal output after leaving and returning to the tab.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/preload/src/index.ts` around lines 346 - 394, The bridge currently
registers global ipcRenderer handlers and only deletes entries from
onDataCallbacksShellInAgentWorkspace on 'agent-workspace:terminal-onEnd', which
leaks closures if sessions fail or are rebound; add an explicit dispose/unbind
API and per-callback listener cleanup: expose a new method (e.g.
'shellInAgentWorkspaceDispose' or 'shellInAgentWorkspaceUnbind') that takes the
callbackId, calls ipcInvoke to tell the main process to close the terminal
session, deletes the entry from onDataCallbacksShellInAgentWorkspace, and
removes any per-callback ipcRenderer listeners; to support removal, change the
global ipcRenderer.on usage for 'agent-workspace:terminal-onData' / '-onError' /
'-onEnd' to either store and register per-callback bound listener functions (so
you can call ipcRenderer.removeListener with those functions when disposing) or
wrap handlers to no-op when the callbackId is missing; update usages tied to
onDataCallbacksShellInAgentWorkspaceId, onDataCallbacksShellInAgentWorkspace,
and the exposed 'shellInAgentWorkspace' so the new dispose/unbind flow is
invoked on unmount or on invoke failure.

Comment on lines +57 to +80
test('shows empty screen when workspace is not running', async () => {
render(AgentWorkspaceTerminal, { workspaceId: 'ws-1', screenReaderMode: true });

await waitFor(() => {
expect(screen.getByText('Workspace is not running')).toBeInTheDocument();
});
});

test('calls shellInAgentWorkspace when workspace is running', async () => {
agentWorkspaceStatuses.set('ws-1', 'running');

const sendCallbackId = 42;
shellInAgentWorkspaceMock.mockResolvedValue(sendCallbackId);

render(AgentWorkspaceTerminal, { workspaceId: 'ws-1', screenReaderMode: true });

await waitFor(() => expect(shellInAgentWorkspaceMock).toHaveBeenCalled());
expect(shellInAgentWorkspaceMock).toHaveBeenCalledWith(
'ws-1',
expect.any(Function),
expect.any(Function),
expect.any(Function),
);
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

fd AgentWorkspaceTerminal.svelte | xargs -r rg -n -C3 'agentWorkspaceStatuses|shellInAgentWorkspace|Workspace is not running'
fd agent-workspaces.svelte | xargs -r rg -n -C3 'agentWorkspaceStatuses|startAgentWorkspace|stopAgentWorkspace'

Repository: kortex-hub/kortex

Length of output: 13022


The component gates shellInAgentWorkspace exclusively on the local status map, breaking manual-start scenarios.

AgentWorkspaceTerminal.svelte derives isRunning from agentWorkspaceStatuses.get(workspaceId) ?? 'stopped' (line 30–31), then gates the shellInAgentWorkspace call with if (!isRunning) { return; } (line 68–70). A workspace started outside the app remains 'stopped' in the map and never reaches the API call, showing "Workspace is not running" instead. To support manually or externally started workspaces, add a mechanism to detect and sync running status from the system (e.g., event listener or health check) before blocking terminal initialization.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/renderer/src/lib/agent-workspaces/AgentWorkspaceTerminal.spec.ts`
around lines 57 - 80, AgentWorkspaceTerminal.svelte currently derives isRunning
from agentWorkspaceStatuses.get(workspaceId) and early-returns before calling
shellInAgentWorkspace, which blocks manual/external starts; change the logic in
AgentWorkspaceTerminal.svelte to not exclusively gate shell initialization on
agentWorkspaceStatuses.get — add a fallback health check or external-status sync
(e.g., call a new checkWorkspaceRunning(workspaceId) or subscribe to an external
event) when agentWorkspaceStatuses.get(...) === 'stopped' and only return after
that check confirms stopped; update the initialization flow around isRunning and
the shellInAgentWorkspace call so shellInAgentWorkspace(workspaceId, ...) is
invoked if the health check or synced status indicates running, and ensure the
code references/updates agentWorkspaceStatuses when the external status is
discovered.

@gastoner gastoner force-pushed the connecting_to_workspace branch from 76ce7ff to b7b6534 Compare April 2, 2026 07:34
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (5)
packages/renderer/src/stores/agent-workspace-terminal-store.ts (1)

53-59: ⚠️ Potential issue | 🟡 Minor

Return a fresh array from the store update.

Line 55 reassigns the list parameter, which violates the noParameterAssign lint rule and mixes reassignment with mutation.

🔧 Suggested fix
 export function registerTerminal(terminal: TerminalOfAgentWorkspace): void {
   agentWorkspaceTerminals.update(list => {
-    list = list.filter(t => t.workspaceId !== terminal.workspaceId);
-    list.push(terminal);
-    return list;
+    return [...list.filter(t => t.workspaceId !== terminal.workspaceId), terminal];
   });
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/renderer/src/stores/agent-workspace-terminal-store.ts` around lines
53 - 59, The registerTerminal function currently reassigns the update callback
parameter and mutates the existing array, violating noParameterAssign; change
the update to return a new array instance instead of reassigning or mutating
`list` — for example, use the result of filtering by `t.workspaceId !==
terminal.workspaceId` and then append the new `terminal` via spread/concat so
the update callback returns a fresh array (refer to registerTerminal and
agentWorkspaceTerminals.update).
packages/renderer/src/lib/agent-workspaces/AgentWorkspaceTerminal.svelte (2)

53-65: ⚠️ Potential issue | 🟠 Major

Prevent duplicate input forwarders on reconnect.

Both receiveEndCallback() (line 59-61) and executeShellInWorkspace() (line 78-80) register new onData handlers. After the first reconnect, each keypress is sent multiple times since xterm's onData doesn't replace existing handlers.

Consider tracking the disposable returned by onData and disposing it before registering a new handler:

let inputDisposable: IDisposable | undefined;

// In both locations:
inputDisposable?.dispose();
inputDisposable = shellTerminal.onData(async data => {
  await window.shellInAgentWorkspaceSend(id, data);
});

Also applies to: 78-81

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/renderer/src/lib/agent-workspaces/AgentWorkspaceTerminal.svelte`
around lines 53 - 65, receiveEndCallback and executeShellInWorkspace both add
xterm onData handlers causing duplicate input forwarding after reconnects; fix
by storing the IDisposable returned by shellTerminal.onData in a shared variable
(e.g., inputDisposable), call inputDisposable?.dispose() before registering a
new handler, assign the new disposable to inputDisposable, and also dispose it
when the terminal is torn down; update the code paths that call
window.shellInAgentWorkspace and window.shellInAgentWorkspaceSend (and
references to sendCallbackId and shellTerminal) to reuse this disposable
management so only one onData handler is active.

121-130: ⚠️ Potential issue | 🟠 Major

Remove the window resize handler on destroy.

Lines 121-130 register an anonymous resize listener that's never removed in onDestroy. After navigating away and back, stale handlers accumulate and call fit()/shellInAgentWorkspaceResize() against disposed terminal state.

♻️ Suggested cleanup
 let sendCallbackId: number | undefined;
 let serializeAddon: SerializeAddon;
+let handleResize: (() => void) | undefined;

 // In refreshTerminal():
-  window.addEventListener('resize', () => {
+  handleResize = () => {
     if (currentRouterPath.includes(`/agent-workspaces/${encodeURIComponent(workspaceId)}/terminal`)) {
       fitAddon.fit();
       if (sendCallbackId) {
         window
           .shellInAgentWorkspaceResize(sendCallbackId, shellTerminal.cols, shellTerminal.rows)
           .catch((err: unknown) => console.error(`Error resizing terminal for workspace ${workspaceId}`, err));
       }
     }
-  });
+  };
+  window.addEventListener('resize', handleResize);

 // In onDestroy:
 onDestroy(() => {
+  if (handleResize) {
+    window.removeEventListener('resize', handleResize);
+  }
   const terminalContent = serializeAddon?.serialize() ?? '';

Also applies to: 139-149

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/renderer/src/lib/agent-workspaces/AgentWorkspaceTerminal.svelte`
around lines 121 - 130, The anonymous window 'resize' listener registering
fitAddon.fit() and calling shellInAgentWorkspaceResize(sendCallbackId,
shellTerminal.cols, shellTerminal.rows) should be converted to a named handler
so it can be removed in onDestroy; create a const/let resizeHandler = () => {
... } that contains the currentRouterPath check, fitAddon.fit(), and the
sendCallbackId call, register it with window.addEventListener('resize',
resizeHandler), and call window.removeEventListener('resize', resizeHandler)
inside onDestroy to avoid stale handlers (apply the same change for the other
similar listener around the 139-149 block). Ensure the handler references
workspaceId, shellTerminal, sendCallbackId, and fitAddon from the same scope so
removal works correctly.
packages/main/src/plugin/agent-workspace/agent-workspace-manager.ts (1)

174-215: ⚠️ Potential issue | 🟠 Major

Expose a close IPC for individual terminal sessions.

The only cleanup paths are the process exiting on its own and the manager-wide dispose(). AgentWorkspaceTerminal.svelte only disposes the local xterm instance on destroy, so leaving the terminal tab leaks the child process and reopening the tab starts another shell.

🧹 Suggested handler
     this.ipcHandle(
       'agent-workspace:terminalResize',
       async (_listener: unknown, onDataId: number, width: number, height: number): Promise<void> => {
         const callback = this.terminalCallbacks.get(onDataId);
         if (callback) {
           callback.resize(width, height);
         }
       },
     );
+
+    this.ipcHandle(
+      'agent-workspace:terminalClose',
+      async (_listener: unknown, onDataId: number): Promise<void> => {
+        const proc = this.terminalProcesses.get(onDataId);
+        if (proc && !proc.killed) {
+          proc.kill();
+        }
+        this.terminalProcesses.delete(onDataId);
+        this.terminalCallbacks.delete(onDataId);
+      },
+    );
   }

Pair this with a renderer-side call from AgentWorkspaceTerminal.svelte's onDestroy before clearing sendCallbackId.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/main/src/plugin/agent-workspace/agent-workspace-manager.ts` around
lines 174 - 215, Add an IPC handler to close a single terminal session: create a
new ipcHandle (e.g., 'agent-workspace:terminalClose') that accepts onDataId,
looks up the corresponding entry in terminalProcesses and terminalCallbacks (the
same maps populated in the 'agent-workspace:terminal' handler), terminates/kills
the child process obtained from terminalProcesses.get(onDataId) (or call a close
method on the process returned by shellInAgentWorkspace), and then delete both
terminalProcesses and terminalCallbacks entries and emit the same
'agent-workspace:terminal-onEnd' event to webContents to mirror the existing
cleanup path; ensure this handler coordinates with existing
shellInAgentWorkspace semantics so no resources remain.
packages/preload/src/index.ts (1)

340-394: ⚠️ Potential issue | 🟠 Major

Allow terminal sessions to be explicitly disposed from renderer.

The preload bridge only cleans up callbacks on agent-workspace:terminal-onEnd. If the initial ipcInvoke('agent-workspace:terminal', ...) fails or the terminal view unmounts before onEnd, those closures remain in onDataCallbacksShellInAgentWorkspace. Subsequent mounts have no way to unbind the stale session.

Consider adding a dispose API:

contextBridge.exposeInMainWorld(
  'shellInAgentWorkspaceDispose',
  async (dataId: number): Promise<void> => {
    onDataCallbacksShellInAgentWorkspace.delete(dataId);
    return ipcInvoke('agent-workspace:terminalClose', dataId);
  },
);

This pairs with the suggested agent-workspace:terminalClose handler in agent-workspace-manager.ts.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/preload/src/index.ts` around lines 340 - 394, The bridge never
exposes a dispose API so stale handlers in onDataCallbacksShellInAgentWorkspace
can leak if ipcInvoke('agent-workspace:terminal', ...) fails or the view
unmounts; add a new contextBridge.exposeInMainWorld entry named
'shellInAgentWorkspaceDispose' that takes the dataId, deletes the entry from
onDataCallbacksShellInAgentWorkspace, and calls
ipcInvoke('agent-workspace:terminalClose', dataId) to notify main to clean up;
ensure the new API mirrors existing patterns used by 'shellInAgentWorkspace',
'shellInAgentWorkspaceSend', and 'shellInAgentWorkspaceResize' so renderers can
explicitly unbind stale sessions.
🧹 Nitpick comments (2)
packages/main/src/plugin/agent-workspace/agent-workspace-manager.spec.ts (1)

358-375: Strengthen the dispose test assertion.

The test spawns a process but only asserts expect(true).toBe(true), which doesn't verify that dispose() actually kills the process.

♻️ Suggested improvement
 describe('dispose', () => {
   test('kills active terminal processes', async () => {
     const mockProcess = {
       stdout: { on: vi.fn() },
       stderr: { on: vi.fn() },
       stdin: { write: vi.fn() },
       on: vi.fn(),
       killed: false,
       kill: vi.fn(),
     } as unknown as ChildProcessWithoutNullStreams;
     vi.spyOn(exec, 'createProcess').mockReturnValue(mockProcess);

     await manager.shellInAgentWorkspace('ws-1', vi.fn(), vi.fn(), vi.fn());

     manager.dispose();

-    expect(true).toBe(true);
+    expect(mockProcess.kill).toHaveBeenCalled();
   });
 });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/main/src/plugin/agent-workspace/agent-workspace-manager.spec.ts`
around lines 358 - 375, The test currently spawns a mock process via
exec.createProcess but only asserts a no-op; update the test to verify that
manager.dispose() actually terminates the spawned process by asserting that the
mock process's kill method was called (or its killed flag became true) after
calling manager.dispose(); locate the mockProcess created in the test and the
calls to manager.shellInAgentWorkspace and manager.dispose to add an assertion
like expect(mockProcess.kill).toHaveBeenCalled() (or check mockProcess.killed)
so the test validates disposal behavior.
packages/renderer/src/stores/agent-workspace-terminal-store.ts (1)

32-51: Consider immutable update in subscription cleanup.

The current implementation mutates the array in place with splice. While functional, returning a new array would be more consistent with immutable patterns:

♻️ Immutable cleanup
 agentWorkspaces.subscribe(workspaces => {
-  const terminals = get(agentWorkspaceTerminals);
-  const toRemove: TerminalOfAgentWorkspace[] = [];
-  for (const terminal of terminals) {
-    const found = workspaces.find(ws => ws.id === terminal.workspaceId);
-    if (!found) {
-      toRemove.push(terminal);
-    }
-  }
-
-  for (const terminal of toRemove) {
-    agentWorkspaceTerminals.update(list => {
-      const index = list.indexOf(terminal);
-      if (index > -1) {
-        list.splice(index, 1);
-      }
-      return list;
-    });
-  }
+  agentWorkspaceTerminals.update(terminals =>
+    terminals.filter(t => workspaces.some(ws => ws.id === t.workspaceId)),
+  );
 });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/renderer/src/stores/agent-workspace-terminal-store.ts` around lines
32 - 51, The subscription cleanup mutates the terminals array in place using
list.splice; change it to an immutable update by replacing the update callback
in agentWorkspaceTerminals.update to return a new filtered array (e.g., filter
out TerminalOfAgentWorkspace items collected in toRemove or directly filter by
checking workspaces existence) instead of splicing the existing list—modify the
logic in the agentWorkspaces.subscribe block where terminals, toRemove, and
agentWorkspaceTerminals.update are used so the update returns a new array rather
than mutating the original.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@packages/main/src/plugin/agent-workspace/agent-workspace-manager.ts`:
- Around line 174-215: Add an IPC handler to close a single terminal session:
create a new ipcHandle (e.g., 'agent-workspace:terminalClose') that accepts
onDataId, looks up the corresponding entry in terminalProcesses and
terminalCallbacks (the same maps populated in the 'agent-workspace:terminal'
handler), terminates/kills the child process obtained from
terminalProcesses.get(onDataId) (or call a close method on the process returned
by shellInAgentWorkspace), and then delete both terminalProcesses and
terminalCallbacks entries and emit the same 'agent-workspace:terminal-onEnd'
event to webContents to mirror the existing cleanup path; ensure this handler
coordinates with existing shellInAgentWorkspace semantics so no resources
remain.

In `@packages/preload/src/index.ts`:
- Around line 340-394: The bridge never exposes a dispose API so stale handlers
in onDataCallbacksShellInAgentWorkspace can leak if
ipcInvoke('agent-workspace:terminal', ...) fails or the view unmounts; add a new
contextBridge.exposeInMainWorld entry named 'shellInAgentWorkspaceDispose' that
takes the dataId, deletes the entry from onDataCallbacksShellInAgentWorkspace,
and calls ipcInvoke('agent-workspace:terminalClose', dataId) to notify main to
clean up; ensure the new API mirrors existing patterns used by
'shellInAgentWorkspace', 'shellInAgentWorkspaceSend', and
'shellInAgentWorkspaceResize' so renderers can explicitly unbind stale sessions.

In `@packages/renderer/src/lib/agent-workspaces/AgentWorkspaceTerminal.svelte`:
- Around line 53-65: receiveEndCallback and executeShellInWorkspace both add
xterm onData handlers causing duplicate input forwarding after reconnects; fix
by storing the IDisposable returned by shellTerminal.onData in a shared variable
(e.g., inputDisposable), call inputDisposable?.dispose() before registering a
new handler, assign the new disposable to inputDisposable, and also dispose it
when the terminal is torn down; update the code paths that call
window.shellInAgentWorkspace and window.shellInAgentWorkspaceSend (and
references to sendCallbackId and shellTerminal) to reuse this disposable
management so only one onData handler is active.
- Around line 121-130: The anonymous window 'resize' listener registering
fitAddon.fit() and calling shellInAgentWorkspaceResize(sendCallbackId,
shellTerminal.cols, shellTerminal.rows) should be converted to a named handler
so it can be removed in onDestroy; create a const/let resizeHandler = () => {
... } that contains the currentRouterPath check, fitAddon.fit(), and the
sendCallbackId call, register it with window.addEventListener('resize',
resizeHandler), and call window.removeEventListener('resize', resizeHandler)
inside onDestroy to avoid stale handlers (apply the same change for the other
similar listener around the 139-149 block). Ensure the handler references
workspaceId, shellTerminal, sendCallbackId, and fitAddon from the same scope so
removal works correctly.

In `@packages/renderer/src/stores/agent-workspace-terminal-store.ts`:
- Around line 53-59: The registerTerminal function currently reassigns the
update callback parameter and mutates the existing array, violating
noParameterAssign; change the update to return a new array instance instead of
reassigning or mutating `list` — for example, use the result of filtering by
`t.workspaceId !== terminal.workspaceId` and then append the new `terminal` via
spread/concat so the update callback returns a fresh array (refer to
registerTerminal and agentWorkspaceTerminals.update).

---

Nitpick comments:
In `@packages/main/src/plugin/agent-workspace/agent-workspace-manager.spec.ts`:
- Around line 358-375: The test currently spawns a mock process via
exec.createProcess but only asserts a no-op; update the test to verify that
manager.dispose() actually terminates the spawned process by asserting that the
mock process's kill method was called (or its killed flag became true) after
calling manager.dispose(); locate the mockProcess created in the test and the
calls to manager.shellInAgentWorkspace and manager.dispose to add an assertion
like expect(mockProcess.kill).toHaveBeenCalled() (or check mockProcess.killed)
so the test validates disposal behavior.

In `@packages/renderer/src/stores/agent-workspace-terminal-store.ts`:
- Around line 32-51: The subscription cleanup mutates the terminals array in
place using list.splice; change it to an immutable update by replacing the
update callback in agentWorkspaceTerminals.update to return a new filtered array
(e.g., filter out TerminalOfAgentWorkspace items collected in toRemove or
directly filter by checking workspaces existence) instead of splicing the
existing list—modify the logic in the agentWorkspaces.subscribe block where
terminals, toRemove, and agentWorkspaceTerminals.update are used so the update
returns a new array rather than mutating the original.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 6bd3d55e-1138-4945-98fb-0ac885bda3b7

📥 Commits

Reviewing files that changed from the base of the PR and between 76ce7ff and b7b6534.

📒 Files selected for processing (11)
  • packages/main/src/plugin/agent-workspace/agent-workspace-manager.spec.ts
  • packages/main/src/plugin/agent-workspace/agent-workspace-manager.ts
  • packages/main/src/plugin/util/exec.spec.ts
  • packages/main/src/plugin/util/exec.ts
  • packages/preload/src/index.ts
  • packages/renderer/src/lib/agent-workspaces/AgentWorkspaceDetails.spec.ts
  • packages/renderer/src/lib/agent-workspaces/AgentWorkspaceDetails.svelte
  • packages/renderer/src/lib/agent-workspaces/AgentWorkspaceTerminal.spec.ts
  • packages/renderer/src/lib/agent-workspaces/AgentWorkspaceTerminal.svelte
  • packages/renderer/src/stores/agent-workspace-terminal-store.spec.ts
  • packages/renderer/src/stores/agent-workspace-terminal-store.ts
✅ Files skipped from review due to trivial changes (2)
  • packages/main/src/plugin/util/exec.spec.ts
  • packages/main/src/plugin/util/exec.ts
🚧 Files skipped from review as they are similar to previous changes (4)
  • packages/renderer/src/lib/agent-workspaces/AgentWorkspaceDetails.spec.ts
  • packages/renderer/src/lib/agent-workspaces/AgentWorkspaceDetails.svelte
  • packages/renderer/src/stores/agent-workspace-terminal-store.spec.ts
  • packages/renderer/src/lib/agent-workspaces/AgentWorkspaceTerminal.spec.ts

@gastoner gastoner marked this pull request as draft April 2, 2026 07:54
@gastoner gastoner force-pushed the connecting_to_workspace branch from b7b6534 to d5c3729 Compare April 2, 2026 08:27
@gastoner gastoner marked this pull request as ready for review April 2, 2026 08:28
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (1)
packages/renderer/src/lib/agent-workspaces/AgentWorkspaceTerminal.svelte (1)

36-43: ⚠️ Potential issue | 🟠 Major

Wait for refreshTerminal() before attaching.

The starting -> running effect can fire while refreshTerminal() is still awaiting configuration reads in onMount(). In that window shellTerminal is still unset, so Lines 80-83 dereference it and the first terminal attach can fail. Serialize executeShellInWorkspace() behind terminal initialization, or bail out until the terminal instance exists.

Also applies to: 70-86, 139-142

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/renderer/src/lib/agent-workspaces/AgentWorkspaceTerminal.svelte`
around lines 36 - 43, The effect watching lastStatus/status can call
executeShellInWorkspace before the terminal is ready (refreshTerminal/onMount),
causing shellTerminal dereference errors; update the $effect (the block
referencing lastStatus, status, executeShellInWorkspace) to wait for terminal
initialization by either (a) checking a ready flag or that shellTerminal is
non-null before calling executeShellInWorkspace, or (b) awaiting a promise that
refreshTerminal resolves, so executeShellInWorkspace only runs after
refreshTerminal/onMount completes; reference the symbols lastStatus, status,
executeShellInWorkspace, refreshTerminal, shellTerminal and ensure the
early-bail check prevents terminal attach until shellTerminal is set.
🧹 Nitpick comments (1)
packages/renderer/src/stores/agent-workspace-terminal-store.spec.ts (1)

25-28: Reset agentWorkspaces in beforeEach.

Later cases mutate the shared workspace store, but this setup only clears agentWorkspaceTerminals. That makes the suite order-dependent once another test assumes the default empty workspace list.

♻️ Small isolation fix
 beforeEach(() => {
   vi.resetAllMocks();
+  agentWorkspaces.set([]);
   agentWorkspaceTerminals.set([]);
 });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/renderer/src/stores/agent-workspace-terminal-store.spec.ts` around
lines 25 - 28, The beforeEach currently resets mocks and clears
agentWorkspaceTerminals but not the shared workspace store; add a reset of
agentWorkspaces in the same beforeEach so each test starts with an empty
workspace list (e.g., call agentWorkspaces.set([]) alongside
agentWorkspaceTerminals.set([])) to prevent test-order dependencies when tests
mutate the workspace store.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/renderer/src/lib/agent-workspaces/AgentWorkspaceTerminal.svelte`:
- Around line 103-116: The code creates a fresh shell session via
shellInAgentWorkspace() and ignores any persisted sendCallbackId from
getExistingTerminal(workspaceId), which leaves prior callbacks/processes alive;
fix by checking the saved sendCallbackId from getExistingTerminal and either
reuse it to reattach/resume the session (call shellInAgentWorkspace with the
existing callbackId or an attach API) or, if resuming is unsupported, explicitly
close/detach the prior session in onDestroy by emitting the
agent-workspace:terminal-onEnd/terminal-close with that sendCallbackId; update
getExistingTerminal, shellInAgentWorkspace usage and add onDestroy cleanup to
ensure the saved callbackId is either reused or terminated.
- Around line 55-67: receiveEndCallback currently uses the shared sendCallbackId
and can reopen a shell for an older session; change to capture the session's
callback id/token in a per-invocation closure so only the session that actually
ended can trigger reconnection. Specifically, when calling
window.shellInAgentWorkspace(...) and wiring the onEnd handler, replace the
shared receiveEndCallback usage with an inline function that closes over the
local id (the value returned by the promise) and only proceeds to call
window.shellInAgentWorkspace(...) again if the closed-over id matches the
session that ended; apply the same pattern for the other block (lines 74-85)
where inputDisposable, shellTerminal?.onData and
window.shellInAgentWorkspaceSend are set so each session's onEnd handler
references its own id/token rather than the module-level sendCallbackId.

---

Duplicate comments:
In `@packages/renderer/src/lib/agent-workspaces/AgentWorkspaceTerminal.svelte`:
- Around line 36-43: The effect watching lastStatus/status can call
executeShellInWorkspace before the terminal is ready (refreshTerminal/onMount),
causing shellTerminal dereference errors; update the $effect (the block
referencing lastStatus, status, executeShellInWorkspace) to wait for terminal
initialization by either (a) checking a ready flag or that shellTerminal is
non-null before calling executeShellInWorkspace, or (b) awaiting a promise that
refreshTerminal resolves, so executeShellInWorkspace only runs after
refreshTerminal/onMount completes; reference the symbols lastStatus, status,
executeShellInWorkspace, refreshTerminal, shellTerminal and ensure the
early-bail check prevents terminal attach until shellTerminal is set.

---

Nitpick comments:
In `@packages/renderer/src/stores/agent-workspace-terminal-store.spec.ts`:
- Around line 25-28: The beforeEach currently resets mocks and clears
agentWorkspaceTerminals but not the shared workspace store; add a reset of
agentWorkspaces in the same beforeEach so each test starts with an empty
workspace list (e.g., call agentWorkspaces.set([]) alongside
agentWorkspaceTerminals.set([])) to prevent test-order dependencies when tests
mutate the workspace store.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 4371bfe3-904f-463a-8f4d-ff21b261ba34

📥 Commits

Reviewing files that changed from the base of the PR and between b7b6534 and d5c3729.

📒 Files selected for processing (9)
  • packages/main/src/plugin/agent-workspace/agent-workspace-manager.spec.ts
  • packages/main/src/plugin/agent-workspace/agent-workspace-manager.ts
  • packages/preload/src/index.ts
  • packages/renderer/src/lib/agent-workspaces/AgentWorkspaceDetails.spec.ts
  • packages/renderer/src/lib/agent-workspaces/AgentWorkspaceDetails.svelte
  • packages/renderer/src/lib/agent-workspaces/AgentWorkspaceTerminal.spec.ts
  • packages/renderer/src/lib/agent-workspaces/AgentWorkspaceTerminal.svelte
  • packages/renderer/src/stores/agent-workspace-terminal-store.spec.ts
  • packages/renderer/src/stores/agent-workspace-terminal-store.ts
✅ Files skipped from review due to trivial changes (2)
  • packages/renderer/src/lib/agent-workspaces/AgentWorkspaceDetails.spec.ts
  • packages/renderer/src/lib/agent-workspaces/AgentWorkspaceDetails.svelte
🚧 Files skipped from review as they are similar to previous changes (5)
  • packages/renderer/src/stores/agent-workspace-terminal-store.ts
  • packages/preload/src/index.ts
  • packages/renderer/src/lib/agent-workspaces/AgentWorkspaceTerminal.spec.ts
  • packages/main/src/plugin/agent-workspace/agent-workspace-manager.ts
  • packages/main/src/plugin/agent-workspace/agent-workspace-manager.spec.ts

Comment on lines +55 to +67
function receiveEndCallback(): void {
if (sendCallbackId && isRunning) {
window
.shellInAgentWorkspace(workspaceId, createDataCallback(), () => {}, receiveEndCallback)
.then(id => {
sendCallbackId = id;
inputDisposable?.dispose();
inputDisposable = shellTerminal?.onData(async data => {
await window.shellInAgentWorkspaceSend(id, data);
});
})
.catch((err: unknown) => console.error(`Error reopening terminal for workspace ${workspaceId}`, err));
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Reconnect only for the session that actually ended.

receiveEndCallback() is reused across every shellInAgentWorkspace() call and only checks the current sendCallbackId. If an older session ends after a newer one was already started, this callback opens another shell and leaves both sessions live. The preload onEnd callback has no payload, so the active callback id or a session token needs to be closed over per invocation.

Also applies to: 74-85

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/renderer/src/lib/agent-workspaces/AgentWorkspaceTerminal.svelte`
around lines 55 - 67, receiveEndCallback currently uses the shared
sendCallbackId and can reopen a shell for an older session; change to capture
the session's callback id/token in a per-invocation closure so only the session
that actually ended can trigger reconnection. Specifically, when calling
window.shellInAgentWorkspace(...) and wiring the onEnd handler, replace the
shared receiveEndCallback usage with an inline function that closes over the
local id (the value returned by the promise) and only proceeds to call
window.shellInAgentWorkspace(...) again if the closed-over id matches the
session that ended; apply the same pattern for the other block (lines 74-85)
where inputDisposable, shellTerminal?.onData and
window.shellInAgentWorkspaceSend are set so each session's onEnd handler
references its own id/token rather than the module-level sendCallbackId.

Comment on lines +103 to +116
const existingTerminal = getExistingTerminal(workspaceId);

shellTerminal = new Terminal({
fontSize,
lineHeight,
screenReaderMode,
theme: getTerminalTheme(),
scrollback,
});

if (existingTerminal) {
shellTerminal.options = { fontSize, lineHeight };
shellTerminal.write(existingTerminal.terminal);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

The saved callbackId is never reused or closed.

Lines 149-153 persist sendCallbackId, but Line 141 always starts a fresh shellInAgentWorkspace() session and the restore path only replays serialized text. With the current preload/main lifecycle, navigating away and back can leave the previous callback/process alive until agent-workspace:terminal-onEnd. Either resume the saved callback id or add an explicit close/detach path in onDestroy().

Also applies to: 139-153

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/renderer/src/lib/agent-workspaces/AgentWorkspaceTerminal.svelte`
around lines 103 - 116, The code creates a fresh shell session via
shellInAgentWorkspace() and ignores any persisted sendCallbackId from
getExistingTerminal(workspaceId), which leaves prior callbacks/processes alive;
fix by checking the saved sendCallbackId from getExistingTerminal and either
reuse it to reattach/resume the session (call shellInAgentWorkspace with the
existing callbackId or an attach API) or, if resuming is unsupported, explicitly
close/detach the prior session in onDestroy by emitting the
agent-workspace:terminal-onEnd/terminal-close with that sendCallbackId; update
getExistingTerminal, shellInAgentWorkspace usage and add onDestroy cleanup to
ensure the saved callbackId is either reused or terminated.

…inal command

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Evzen Gasta <evzen.ml@seznam.cz>
@gastoner gastoner force-pushed the connecting_to_workspace branch from d5c3729 to b2ac27f Compare April 2, 2026 08:44
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
packages/renderer/src/stores/agent-workspace-terminal-store.ts (1)

42-50: Prefer immutable update pattern for consistency.

Lines 43-48 mutate the array with splice inside update(), while registerTerminal (line 55) correctly uses an immutable spread pattern. For consistency and to avoid potential Svelte reactivity edge cases, consider using the same immutable pattern here.

Suggested change
-  for (const terminal of toRemove) {
-    agentWorkspaceTerminals.update(list => {
-      const index = list.indexOf(terminal);
-      if (index > -1) {
-        list.splice(index, 1);
-      }
-      return list;
-    });
-  }
+  if (toRemove.length > 0) {
+    agentWorkspaceTerminals.update(list =>
+      list.filter(t => !toRemove.includes(t))
+    );
+  }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/renderer/src/stores/agent-workspace-terminal-store.ts` around lines
42 - 50, The current loop mutates the agentWorkspaceTerminals list with splice;
instead, follow the immutable pattern used in registerTerminal by producing a
new array. Convert toRemove into a Set for O(1) checks (or use item equality),
then call agentWorkspaceTerminals.update(list => list.filter(item =>
!toRemoveSet.has(item))) so you return a new array rather than splicing
in-place; this also lets you perform a single update instead of one per
terminal.
packages/main/src/plugin/agent-workspace/agent-workspace-manager.ts (1)

80-89: Consider handling malformed JSON gracefully.

If workspace.json exists but contains invalid JSON, JSON.parse throws a SyntaxError that propagates to the caller. Depending on expected usage, you may want to catch parse errors and return a fallback or wrap with a clearer error message.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/main/src/plugin/agent-workspace/agent-workspace-manager.ts` around
lines 80 - 89, The current try/catch around reading and parsing workspace.json
can let a SyntaxError from JSON.parse bubble up; update the error handling in
the block that reads configPath (using readFile and JSON.parse) inside
agent-workspace-manager so that a JSON parse failure is handled explicitly:
catch SyntaxError and either return a safe fallback (e.g., { name:
workspace.name } as AgentWorkspaceConfiguration) or throw a new Error that
clearly indicates "malformed workspace.json" and includes the original error
message; keep the existing ENOENT branch unchanged. Ensure you reference the
same symbols (configPath, readFile, JSON.parse, AgentWorkspaceConfiguration,
workspace.paths.configuration, workspace.name) when making the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@packages/main/src/plugin/agent-workspace/agent-workspace-manager.ts`:
- Around line 80-89: The current try/catch around reading and parsing
workspace.json can let a SyntaxError from JSON.parse bubble up; update the error
handling in the block that reads configPath (using readFile and JSON.parse)
inside agent-workspace-manager so that a JSON parse failure is handled
explicitly: catch SyntaxError and either return a safe fallback (e.g., { name:
workspace.name } as AgentWorkspaceConfiguration) or throw a new Error that
clearly indicates "malformed workspace.json" and includes the original error
message; keep the existing ENOENT branch unchanged. Ensure you reference the
same symbols (configPath, readFile, JSON.parse, AgentWorkspaceConfiguration,
workspace.paths.configuration, workspace.name) when making the change.

In `@packages/renderer/src/stores/agent-workspace-terminal-store.ts`:
- Around line 42-50: The current loop mutates the agentWorkspaceTerminals list
with splice; instead, follow the immutable pattern used in registerTerminal by
producing a new array. Convert toRemove into a Set for O(1) checks (or use item
equality), then call agentWorkspaceTerminals.update(list => list.filter(item =>
!toRemoveSet.has(item))) so you return a new array rather than splicing
in-place; this also lets you perform a single update instead of one per
terminal.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: ab06aed9-d4d9-48af-8d5e-80bcdebef3dc

📥 Commits

Reviewing files that changed from the base of the PR and between d5c3729 and b2ac27f.

📒 Files selected for processing (9)
  • packages/main/src/plugin/agent-workspace/agent-workspace-manager.spec.ts
  • packages/main/src/plugin/agent-workspace/agent-workspace-manager.ts
  • packages/preload/src/index.ts
  • packages/renderer/src/lib/agent-workspaces/AgentWorkspaceDetails.spec.ts
  • packages/renderer/src/lib/agent-workspaces/AgentWorkspaceDetails.svelte
  • packages/renderer/src/lib/agent-workspaces/AgentWorkspaceTerminal.spec.ts
  • packages/renderer/src/lib/agent-workspaces/AgentWorkspaceTerminal.svelte
  • packages/renderer/src/stores/agent-workspace-terminal-store.spec.ts
  • packages/renderer/src/stores/agent-workspace-terminal-store.ts
✅ Files skipped from review due to trivial changes (3)
  • packages/renderer/src/lib/agent-workspaces/AgentWorkspaceDetails.spec.ts
  • packages/renderer/src/lib/agent-workspaces/AgentWorkspaceDetails.svelte
  • packages/renderer/src/lib/agent-workspaces/AgentWorkspaceTerminal.spec.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/preload/src/index.ts
  • packages/main/src/plugin/agent-workspace/agent-workspace-manager.spec.ts

const childProcess = spawn('kortex-cli', [
'terminal',
id,
'--',
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: why not using the default agent command (I would expect to see claude running ?)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Also connecting to a running workspace from the workspace detail page

2 participants