Skip to content

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

Open
gastoner wants to merge 3 commits intoopenkaiden:mainfrom
gastoner:connecting_to_workspace
Open

feat(Workspaces): added connecting to workspace using kortex-cli terminal#1222
gastoner wants to merge 3 commits intoopenkaiden: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
Contributor

coderabbitai bot commented Apr 2, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds interactive terminal support for agent workspaces: main process spawns per-workspace CLI terminals and exposes IPC for lifecycle and I/O; preload bridges terminal APIs to the renderer; renderer adds a terminal component, routes, and persistent terminal store plus tests.

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
Load workspace configuration from workspace.json (JSON parse, ENOENT -> { name } fallback); add shellInAgentWorkspace() to spawn kortex-cli terminal <id> and forward stdout/stderr/errors/end; add IPC handlers agent-workspace:terminal, agent-workspace:terminalSend, agent-workspace:terminalResize; track and kill subprocesses in dispose(); tests updated to mock spawn, webContents, and file reads.
Preload Bridge
packages/preload/src/index.ts
Expose shellInAgentWorkspace, shellInAgentWorkspaceSend, shellInAgentWorkspaceResize via contextBridge; register ipcRenderer.on listeners for agent-workspace:terminal-onData, -onError, -onEnd and route callbacks by numeric callbackId.
Renderer — Details & Tab
packages/renderer/src/lib/agent-workspaces/AgentWorkspaceDetails.svelte, packages/renderer/src/lib/agent-workspaces/AgentWorkspaceDetails.spec.ts
Add "Terminal" tab and route to workspace details; test asserts Terminal tab renders.
Renderer — Terminal Component & Tests
packages/renderer/src/lib/agent-workspaces/AgentWorkspaceTerminal.svelte, packages/renderer/src/lib/agent-workspaces/AgentWorkspaceTerminal.spec.ts
New AgentWorkspaceTerminal component integrating xterm.js: loads config, restores serialized content, opens shell via window.shellInAgentWorkspace, forwards input/resizes, handles lifecycle (reopen on end), and serializes content on destroy; 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
Add agentWorkspaceTerminals writable store, TerminalOfAgentWorkspace type, registerTerminal(), getExistingTerminal() and cleanup that removes terminals when workspaces are deleted; tests cover register/overwrite/retrieval/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(workspaceId, onData,onError,onEnd)
    Preload->>Main: ipcInvoke('agent-workspace:terminal', workspaceId, callbackId)
    Main->>CLI: spawn('kortex-cli', ['terminal', workspaceId])
    Main->>Main: store callbacks & process keyed 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 or re-register session
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

🚥 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 title accurately summarizes the main change: adding terminal connectivity to running workspaces using kortex-cli.
Description check ✅ Passed The description is related to the changeset, explaining the feature, testing instructions, and linking to the resolved issue.
Linked Issues check ✅ Passed The PR implementation meets all requirements from issue #1125: enables terminal connection to running workspaces from the detail page using CLI integration.
Out of Scope Changes check ✅ Passed All changes are focused on implementing the terminal feature; no unrelated modifications detected beyond the scope of issue #1125.

✏️ 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
Contributor

@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
Contributor

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
Contributor

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
Contributor

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
Contributor

@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
Contributor

@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
Contributor

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
Contributor

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.

@gastoner gastoner force-pushed the connecting_to_workspace branch from d5c3729 to b2ac27f Compare April 2, 2026 08:44
Copy link
Copy Markdown
Contributor

@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 ?)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Updated

Copy link
Copy Markdown
Contributor

@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/main/src/plugin/agent-workspace/agent-workspace-manager.ts (1)

168-209: ⚠️ Potential issue | 🟠 Major

Expose an IPC handler to close individual terminal sessions.

The terminal IPC handlers register processes but only clean up when the process exits on its own or when dispose() is called. If the user closes the terminal tab in the UI, the child process continues running because there's no agent-workspace:terminalClose handler. Reopening the tab spawns another shell, leaking processes.

🧹 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 168 - 209, Add a new IPC handler named 'agent-workspace:terminalClose'
that accepts onDataId, looks up the associated process in terminalProcesses and
the callbacks in terminalCallbacks (the same maps populated by
shellInAgentWorkspace), attempts to terminate the child process (e.g., call
process.kill() or an appropriate close method on the stored process object), and
then removes the entries from terminalProcesses and terminalCallbacks; ensure
the handler is no-op if onDataId is not found and send any error back via
webContents if kill fails. Also call this IPC from AgentWorkspaceTerminal.svelte
onDestroy before clearing sendCallbackId so closing the UI tab triggers the
cleanup.
🤖 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 302-317: Add a new test mirroring the stdout test to verify stderr
is also forwarded to the onData callback: create a Map for stderr listeners,
mock a process via createMockProcess with stderrOn set to a vi.fn that stores
callbacks in that Map, have vi.mocked(spawn).mockReturnValue(mockProcess), call
manager.shellInAgentWorkspace('ws-1', onData, vi.fn(), vi.fn()), retrieve the
'data' callback from the stderr listeners and invoke it with Buffer.from('error
output'), then expect(onData).toHaveBeenCalledWith('error output') to confirm
stderr wiring.

In `@packages/main/src/plugin/agent-workspace/agent-workspace-manager.ts`:
- Around line 104-142: shellInAgentWorkspace's returned write implementation
currently calls childProcess.stdin.write without handling failures; update the
write function in shellInAgentWorkspace to guard and report write errors by (1)
checking childProcess.stdin.writable and !childProcess.stdin.destroyed before
writing, (2) invoking childProcess.stdin.write(param, callback) and in the
callback call onError(err.message) if err is provided, and (3) wrapping the
write call in try/catch to call onError for synchronous exceptions — reference
the shellInAgentWorkspace function and its returned write method and ensure you
do not change the existing onError/onData/onEnd callbacks' signatures.

---

Duplicate comments:
In `@packages/main/src/plugin/agent-workspace/agent-workspace-manager.ts`:
- Around line 168-209: Add a new IPC handler named
'agent-workspace:terminalClose' that accepts onDataId, looks up the associated
process in terminalProcesses and the callbacks in terminalCallbacks (the same
maps populated by shellInAgentWorkspace), attempts to terminate the child
process (e.g., call process.kill() or an appropriate close method on the stored
process object), and then removes the entries from terminalProcesses and
terminalCallbacks; ensure the handler is no-op if onDataId is not found and send
any error back via webContents if kill fails. Also call this IPC from
AgentWorkspaceTerminal.svelte onDestroy before clearing sendCallbackId so
closing the UI tab triggers the cleanup.
🪄 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: Repository UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: d456502b-be5a-4b94-a39d-e1c0bd9ad1c5

📥 Commits

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

📒 Files selected for processing (2)
  • packages/main/src/plugin/agent-workspace/agent-workspace-manager.spec.ts
  • packages/main/src/plugin/agent-workspace/agent-workspace-manager.ts
📜 Review details
🧰 Additional context used
📓 Path-based instructions (3)
**/*.ts

📄 CodeRabbit inference engine (AGENTS.md)

Use /@/ path aliases (e.g., '/@/plugin/provider-registry.js') instead of relative paths (e.g., '../plugin/provider-registry.js') for imports outside the current directory's module group. Relative imports are only used for sibling modules within the same directory (e.g., './chat-manager.js').

Files:

  • packages/main/src/plugin/agent-workspace/agent-workspace-manager.spec.ts
  • packages/main/src/plugin/agent-workspace/agent-workspace-manager.ts
**/*.spec.ts

📄 CodeRabbit inference engine (AGENTS.md)

**/*.spec.ts: Use test() function instead of it() for test cases in Vitest
Use vi.mock(import('...')) for auto-mocking modules. Avoid manual mock factories (vi.mock('...', () => ({...}))) when possible.
Use vi.resetAllMocks() in beforeEach hooks, not vi.clearAllMocks(), for resetting mocks between tests.
When an auto-mocked function or class method needs a real implementation, use vi.mocked(...). For class methods, use the prototype pattern: vi.mocked(MyClass.prototype.myMethod).mockImplementation(...).

Files:

  • packages/main/src/plugin/agent-workspace/agent-workspace-manager.spec.ts
packages/main/src/**/*.ts

📄 CodeRabbit inference engine (AGENTS.md)

IPC handlers must follow the naming convention <registry-name>:<action> (e.g., container-provider-registry:listContainers) when exposed via ipcHandle()

Files:

  • packages/main/src/plugin/agent-workspace/agent-workspace-manager.spec.ts
  • packages/main/src/plugin/agent-workspace/agent-workspace-manager.ts
🧠 Learnings (7)
📚 Learning: 2026-04-08T15:23:51.417Z
Learnt from: CR
Repo: openkaiden/kaiden PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-04-08T15:23:51.417Z
Learning: Applies to **/*.spec.ts : Use `vi.resetAllMocks()` in `beforeEach` hooks, not `vi.clearAllMocks()`, for resetting mocks between tests.

Applied to files:

  • packages/main/src/plugin/agent-workspace/agent-workspace-manager.spec.ts
📚 Learning: 2026-04-08T15:23:51.417Z
Learnt from: CR
Repo: openkaiden/kaiden PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-04-08T15:23:51.417Z
Learning: Applies to **/*.spec.ts : Use `vi.mock(import('...'))` for auto-mocking modules. Avoid manual mock factories (`vi.mock('...', () => ({...}))`) when possible.

Applied to files:

  • packages/main/src/plugin/agent-workspace/agent-workspace-manager.spec.ts
📚 Learning: 2026-04-08T15:23:51.417Z
Learnt from: CR
Repo: openkaiden/kaiden PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-04-08T15:23:51.417Z
Learning: Applies to **/*.spec.ts : When an auto-mocked function or class method needs a real implementation, use `vi.mocked(...)`. For class methods, use the prototype pattern: `vi.mocked(MyClass.prototype.myMethod).mockImplementation(...)`.

Applied to files:

  • packages/main/src/plugin/agent-workspace/agent-workspace-manager.spec.ts
📚 Learning: 2026-04-08T15:23:51.417Z
Learnt from: CR
Repo: openkaiden/kaiden PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-04-08T15:23:51.417Z
Learning: Applies to **/*.spec.ts : Use `test()` function instead of `it()` for test cases in Vitest

Applied to files:

  • packages/main/src/plugin/agent-workspace/agent-workspace-manager.spec.ts
📚 Learning: 2026-04-08T15:23:51.417Z
Learnt from: CR
Repo: openkaiden/kaiden PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-04-08T15:23:51.417Z
Learning: Applies to tests/playwright/src/**/*.ts : E2E tests should follow the Page Object Model pattern. See `.agents/skills/playwright-testing/` for detailed guidance on fixtures, locator conventions, and examples.

Applied to files:

  • packages/main/src/plugin/agent-workspace/agent-workspace-manager.spec.ts
📚 Learning: 2026-04-08T15:23:51.417Z
Learnt from: CR
Repo: openkaiden/kaiden PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-04-08T15:23:51.417Z
Learning: Applies to packages/main/src/**/*.ts : IPC handlers must follow the naming convention `<registry-name>:<action>` (e.g., `container-provider-registry:listContainers`) when exposed via `ipcHandle()`

Applied to files:

  • packages/main/src/plugin/agent-workspace/agent-workspace-manager.spec.ts
  • packages/main/src/plugin/agent-workspace/agent-workspace-manager.ts
📚 Learning: 2026-03-09T08:47:09.657Z
Learnt from: benoitf
Repo: kortex-hub/kortex PR: 1077
File: packages/main/src/plugin/skill/skill-manager.ts:80-109
Timestamp: 2026-03-09T08:47:09.657Z
Learning: In the kortex-hub/kortex repository, IPC handlers (via ipcHandle()) may be registered directly inside feature manager/service classes (e.g., SkillManager in packages/main/src/plugin/skill/skill-manager.ts) rather than exclusively in packages/main/src/plugin/index.ts. Treat this as an accepted design pattern for files under the plugin directory. Reviewers should not require centralization in index.ts; allow IPC registration proximity to the feature that owns the handler. When reviewing code, accept direct ipcHandle() registrations inside feature managers and ensure the pattern is consistently applied across similar feature-manager modules.

Applied to files:

  • packages/main/src/plugin/agent-workspace/agent-workspace-manager.spec.ts
  • packages/main/src/plugin/agent-workspace/agent-workspace-manager.ts
🔇 Additional comments (9)
packages/main/src/plugin/agent-workspace/agent-workspace-manager.ts (3)

41-56: LGTM!

The new terminal state management properties (terminalCallbacks, terminalProcesses) and WebContents injection are well-structured for tracking spawned processes and their callbacks.


74-90: LGTM!

The configuration loading correctly handles the ENOENT case by returning a fallback configuration with the workspace name, while re-throwing other errors. The error type narrowing via NodeJS.ErrnoException is appropriate.


212-221: LGTM!

The dispose() method correctly iterates over all tracked terminal processes, kills those that haven't already been killed, and clears both maps. This ensures proper cleanup on manager destruction.

packages/main/src/plugin/agent-workspace/agent-workspace-manager.spec.ts (6)

36-37: LGTM!

Correctly uses vi.mock(import('...')) for auto-mocking modules as per coding guidelines.


73-77: LGTM!

Uses vi.resetAllMocks() in beforeEach as required by coding guidelines, and correctly passes the new webContents dependency to the constructor.


99-110: LGTM!

Good coverage for verifying the new terminal-related IPC handlers are registered with the correct naming convention.


189-205: LGTM!

Excellent test coverage for the ENOENT fallback behavior and non-ENOENT error propagation. The error construction with explicit code property correctly simulates NodeJS.ErrnoException.


258-272: LGTM!

The createMockProcess helper is well-structured with proper typing (ReturnType<typeof vi.fn>) and sensible defaults for all required properties.


354-380: LGTM!

Good test for verifying that dispose() kills active terminal processes. The approach of extracting the IPC handler from the mock calls and invoking it to set up state before testing dispose() is appropriate.

Comment on lines +302 to +317
test('calls onData when stdout emits data', () => {
const stdoutListeners = new Map<string, (data: Buffer) => void>();
const mockProcess = createMockProcess({
stdoutOn: vi.fn((event: string, cb: (data: Buffer) => void) => stdoutListeners.set(event, cb)),
});
vi.mocked(spawn).mockReturnValue(mockProcess);

const onData = vi.fn();
manager.shellInAgentWorkspace('ws-1', onData, vi.fn(), vi.fn());

const dataCallback = stdoutListeners.get('data');
expect(dataCallback).toBeDefined();
dataCallback!(Buffer.from('output'));

expect(onData).toHaveBeenCalledWith('output');
});
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.

🧹 Nitpick | 🔵 Trivial

Add test coverage for stderr forwarding to onData.

The implementation forwards both stdout and stderr data to the onData callback (lines 117-123 in the implementation), but only stdout is tested here. Consider adding a test for stderr to ensure both streams are properly wired.

✅ Suggested test
test('calls onData when stderr emits data', () => {
  const stderrListeners = new Map<string, (data: Buffer) => void>();
  const mockProcess = createMockProcess({
    stderrOn: vi.fn((event: string, cb: (data: Buffer) => void) => stderrListeners.set(event, cb)),
  });
  vi.mocked(spawn).mockReturnValue(mockProcess);

  const onData = vi.fn();
  manager.shellInAgentWorkspace('ws-1', onData, vi.fn(), vi.fn());

  const dataCallback = stderrListeners.get('data');
  expect(dataCallback).toBeDefined();
  dataCallback!(Buffer.from('error output'));

  expect(onData).toHaveBeenCalledWith('error output');
});
🤖 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 302 - 317, Add a new test mirroring the stdout test to verify
stderr is also forwarded to the onData callback: create a Map for stderr
listeners, mock a process via createMockProcess with stderrOn set to a vi.fn
that stores callbacks in that Map, have
vi.mocked(spawn).mockReturnValue(mockProcess), call
manager.shellInAgentWorkspace('ws-1', onData, vi.fn(), vi.fn()), retrieve the
'data' callback from the stderr listeners and invoke it with Buffer.from('error
output'), then expect(onData).toHaveBeenCalledWith('error output') to confirm
stderr wiring.

Comment on lines +104 to +142
shellInAgentWorkspace(
id: string,
onData: (data: string) => void,
onError: (error: string) => void,
onEnd: () => void,
): {
write: (param: string) => void;
resize: (w: number, h: number) => void;
process: ChildProcessWithoutNullStreams;
} {
// eslint-disable-next-line sonarjs/no-os-command-from-path
const childProcess = spawn('kortex-cli', ['terminal', id]);

childProcess.stdout.on('data', (chunk: Buffer) => {
onData(chunk.toString('utf-8'));
});

childProcess.stderr.on('data', (chunk: Buffer) => {
onData(chunk.toString('utf-8'));
});

childProcess.on('error', (error: Error) => {
onError(error.message);
});

childProcess.on('close', () => {
onEnd();
});

return {
write: (param: string): void => {
childProcess.stdin.write(param);
},
resize: (_w: number, _h: number): void => {
// no-op: resize requires a PTY (e.g. node-pty); can be added later
},
process: childProcess,
};
}
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.

🧹 Nitpick | 🔵 Trivial

Consider adding error handling for stdin.write failures.

stdin.write can fail (e.g., if the child process has already exited or the stream is closed), but the error is silently ignored. This could lead to data loss without any indication to the caller.

♻️ Suggested improvement
     return {
       write: (param: string): void => {
-        childProcess.stdin.write(param);
+        childProcess.stdin.write(param, err => {
+          if (err) {
+            onError(err.message);
+          }
+        });
       },
       resize: (_w: number, _h: number): void => {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
shellInAgentWorkspace(
id: string,
onData: (data: string) => void,
onError: (error: string) => void,
onEnd: () => void,
): {
write: (param: string) => void;
resize: (w: number, h: number) => void;
process: ChildProcessWithoutNullStreams;
} {
// eslint-disable-next-line sonarjs/no-os-command-from-path
const childProcess = spawn('kortex-cli', ['terminal', id]);
childProcess.stdout.on('data', (chunk: Buffer) => {
onData(chunk.toString('utf-8'));
});
childProcess.stderr.on('data', (chunk: Buffer) => {
onData(chunk.toString('utf-8'));
});
childProcess.on('error', (error: Error) => {
onError(error.message);
});
childProcess.on('close', () => {
onEnd();
});
return {
write: (param: string): void => {
childProcess.stdin.write(param);
},
resize: (_w: number, _h: number): void => {
// no-op: resize requires a PTY (e.g. node-pty); can be added later
},
process: childProcess,
};
}
shellInAgentWorkspace(
id: string,
onData: (data: string) => void,
onError: (error: string) => void,
onEnd: () => void,
): {
write: (param: string) => void;
resize: (w: number, h: number) => void;
process: ChildProcessWithoutNullStreams;
} {
// eslint-disable-next-line sonarjs/no-os-command-from-path
const childProcess = spawn('kortex-cli', ['terminal', id]);
childProcess.stdout.on('data', (chunk: Buffer) => {
onData(chunk.toString('utf-8'));
});
childProcess.stderr.on('data', (chunk: Buffer) => {
onData(chunk.toString('utf-8'));
});
childProcess.on('error', (error: Error) => {
onError(error.message);
});
childProcess.on('close', () => {
onEnd();
});
return {
write: (param: string): void => {
childProcess.stdin.write(param, err => {
if (err) {
onError(err.message);
}
});
},
resize: (_w: number, _h: number): void => {
// no-op: resize requires a PTY (e.g. node-pty); can be added later
},
process: childProcess,
};
}
🤖 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 104 - 142, shellInAgentWorkspace's returned write implementation currently
calls childProcess.stdin.write without handling failures; update the write
function in shellInAgentWorkspace to guard and report write errors by (1)
checking childProcess.stdin.writable and !childProcess.stdin.destroyed before
writing, (2) invoking childProcess.stdin.write(param, callback) and in the
callback call onError(err.message) if err is provided, and (3) wrapping the
write call in try/catch to call onError for synchronous exceptions — reference
the shellInAgentWorkspace function and its returned write method and ensure you
do not change the existing onError/onData/onEnd callbacks' signatures.

@gastoner gastoner force-pushed the connecting_to_workspace branch from a01d75d to 538d759 Compare April 9, 2026 07:16
@gastoner gastoner requested a review from jeffmaury April 9, 2026 07:17
@codecov
Copy link
Copy Markdown

codecov bot commented Apr 9, 2026

gastoner and others added 2 commits April 9, 2026 13:19
…inal command

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
Signed-off-by: Evzen Gasta <[email protected]>
Signed-off-by: Evzen Gasta <[email protected]>
@gastoner gastoner force-pushed the connecting_to_workspace branch 2 times, most recently from 91f37bf to d12b446 Compare April 9, 2026 12:15
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
Signed-off-by: Evzen Gasta <[email protected]>
@gastoner gastoner force-pushed the connecting_to_workspace branch from d12b446 to 184020e Compare April 10, 2026 06:52
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