Skip to content

Commit bf50963

Browse files
Merge pull request olasunkanmi-SE#374 from olasunkanmi-SE/feature/terminal_viewer
TerminalViewerPanelTerminalViewerHandleruseTerminalStorelistSessionsD…
2 parents a4ba710 + 29e9c97 commit bf50963

33 files changed

Lines changed: 916 additions & 128 deletions

docs/WEBVIEW_UI_AUDIT.md

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -259,22 +259,6 @@ These services exist in `src/services/` with **no corresponding handler or UI su
259259

260260
---
261261

262-
## 7. Dead Code & Cleanup Candidates
263-
264-
| File | Status | Action |
265-
|------|--------|--------|
266-
| `settings.tsx` (root) | Superseded by `settings/SettingsPanel.tsx` | Delete |
267-
| `AgentActivityFeed.tsx` | Orphaned, replaced by `AgentTimeline.tsx` | Merge unique features into AgentTimeline, then delete |
268-
| `futureFeatures.tsx` | Orphaned, never rendered | Wire into UI or delete |
269-
| `extensions.tsx` | Orphaned, uses mock data | Wire into MCP/Agent management or delete |
270-
| `userMessageTest.tsx` | Test harness, not needed in prod | Move to test directory or delete |
271-
| `thinkingExample.tsx` | Example file, not needed in prod | Move to test directory or delete |
272-
| `thinkingTest.tsx` | Test file, not needed in prod | Move to test directory or delete |
273-
| `THINKING_COMPONENT.md` | Documentation in components dir | Move to docs/ |
274-
| `errorBoundry.tsx` | Typo in filename | Rename to `errorBoundary.tsx` |
275-
276-
---
277-
278262
## Appendix: Full Wiring Map
279263

280264
### Extension Handler → Webview Store/Component Mapping

src/services/deep-terminal.service.ts

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -546,10 +546,10 @@ export class DeepTerminalService extends EventEmitter {
546546
* @param id Session identifier.
547547
* @returns Concatenated new output, or an empty string if none.
548548
*/
549-
public readOutput(id: string): string {
549+
public readOutput(id: string): string | null {
550550
const session = this.sessions.get(id);
551551
if (!session) {
552-
return `Session ${id} not found.`;
552+
return null;
553553
}
554554

555555
const newLines = session.outputBuffer.slice(session.lastReadIndex);
@@ -567,14 +567,29 @@ export class DeepTerminalService extends EventEmitter {
567567
*
568568
* @param id Session identifier.
569569
*/
570-
public getFullHistory(id: string): string {
570+
public getFullHistory(id: string): string | null {
571571
const session = this.sessions.get(id);
572572
if (!session) {
573-
return `Session ${id} not found.`;
573+
return null;
574574
}
575575
return session.outputBuffer.toArray().join("");
576576
}
577577

578+
/**
579+
* Returns metadata for all active sessions (for webview display).
580+
*/
581+
public listSessions(): Array<{
582+
id: string;
583+
createdAt: number;
584+
bufferSize: number;
585+
}> {
586+
return [...this.sessions.entries()].map(([id, s]) => ({
587+
id,
588+
createdAt: s.createdAt,
589+
bufferSize: s.outputBuffer.length,
590+
}));
591+
}
592+
578593
/**
579594
* Kills the session's child process and removes it from the registry.
580595
*

src/test/suite/deep-terminal.service.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -507,9 +507,9 @@ suite("DeepTerminalService — lifecycle", () => {
507507
assert.strictEqual(service.readOutput("ro1"), "");
508508
});
509509

510-
test("readOutput returns not-found for missing session", () => {
510+
test("readOutput returns null for missing session", () => {
511511
const out = service.readOutput("missing");
512-
assert.ok(out.includes("not found"));
512+
assert.strictEqual(out, null);
513513
});
514514

515515
test("singleton identity", () => {
Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
import * as assert from "assert";
2+
import * as sinon from "sinon";
3+
import { TerminalViewerHandler } from "../../webview-providers/handlers/terminal-viewer-handler";
4+
import { HandlerContext } from "../../webview-providers/handlers/types";
5+
import { DeepTerminalService } from "../../services/deep-terminal.service";
6+
7+
suite("TerminalViewerHandler", () => {
8+
let handler: TerminalViewerHandler;
9+
let ctx: HandlerContext;
10+
let postMessageStub: sinon.SinonStub;
11+
let serviceStub: sinon.SinonStubbedInstance<DeepTerminalService>;
12+
13+
setup(() => {
14+
handler = new TerminalViewerHandler();
15+
16+
postMessageStub = sinon.stub().resolves(true);
17+
ctx = {
18+
webview: { webview: { postMessage: postMessageStub } },
19+
logger: { warn: sinon.stub(), info: sinon.stub(), error: sinon.stub() },
20+
extensionUri: {} as any,
21+
sendResponse: sinon.stub(),
22+
} as unknown as HandlerContext;
23+
24+
serviceStub = sinon.createStubInstance(DeepTerminalService);
25+
sinon.stub(DeepTerminalService, "getInstance").returns(serviceStub as any);
26+
});
27+
28+
teardown(() => {
29+
sinon.restore();
30+
});
31+
32+
// ── commands ────────────────────────────────────────────────────
33+
34+
test("registers all three terminal commands", () => {
35+
assert.ok(handler.commands.includes("terminal-list-sessions"));
36+
assert.ok(handler.commands.includes("terminal-session-history"));
37+
assert.ok(handler.commands.includes("terminal-session-output"));
38+
assert.strictEqual(handler.commands.length, 3);
39+
});
40+
41+
// ── terminal-list-sessions ─────────────────────────────────────
42+
43+
test("list-sessions posts empty array when no sessions", async () => {
44+
serviceStub.listSessions.returns([]);
45+
46+
await handler.handle({ command: "terminal-list-sessions" }, ctx);
47+
48+
assert.ok(postMessageStub.calledOnce);
49+
const msg = postMessageStub.firstCall.args[0];
50+
assert.strictEqual(msg.type, "terminal-list-sessions-result");
51+
assert.deepStrictEqual(msg.sessions, []);
52+
});
53+
54+
test("list-sessions posts session array", async () => {
55+
const sessions = [
56+
{ id: "s1", createdAt: 1000, bufferSize: 100 },
57+
{ id: "s2", createdAt: 2000, bufferSize: 200 },
58+
];
59+
serviceStub.listSessions.returns(sessions);
60+
61+
await handler.handle({ command: "terminal-list-sessions" }, ctx);
62+
63+
assert.ok(postMessageStub.calledOnce);
64+
const msg = postMessageStub.firstCall.args[0];
65+
assert.strictEqual(msg.type, "terminal-list-sessions-result");
66+
assert.deepStrictEqual(msg.sessions, sessions);
67+
});
68+
69+
// ── terminal-session-history ───────────────────────────────────
70+
71+
test("history returns full output for valid session", async () => {
72+
serviceStub.getFullHistory.returns("$ echo hello\nhello\n");
73+
74+
await handler.handle(
75+
{ command: "terminal-session-history", sessionId: "s1" },
76+
ctx,
77+
);
78+
79+
assert.ok(postMessageStub.calledOnce);
80+
const msg = postMessageStub.firstCall.args[0];
81+
assert.strictEqual(msg.type, "terminal-session-history-result");
82+
assert.strictEqual(msg.sessionId, "s1");
83+
assert.strictEqual(msg.output, "$ echo hello\nhello\n");
84+
});
85+
86+
test("history returns error for missing session", async () => {
87+
serviceStub.getFullHistory.returns(null);
88+
89+
await handler.handle(
90+
{ command: "terminal-session-history", sessionId: "missing" },
91+
ctx,
92+
);
93+
94+
assert.ok(postMessageStub.calledOnce);
95+
const msg = postMessageStub.firstCall.args[0];
96+
assert.strictEqual(msg.type, "terminal-error");
97+
assert.strictEqual(msg.sessionId, "missing");
98+
assert.ok(msg.error.includes("not found"));
99+
});
100+
101+
test("history returns error for empty sessionId", async () => {
102+
await handler.handle(
103+
{ command: "terminal-session-history", sessionId: " " },
104+
ctx,
105+
);
106+
107+
assert.ok(postMessageStub.calledOnce);
108+
const msg = postMessageStub.firstCall.args[0];
109+
assert.strictEqual(msg.type, "terminal-error");
110+
assert.strictEqual(msg.sessionId, null);
111+
assert.ok(msg.error.includes("Invalid"));
112+
});
113+
114+
test("history returns error for non-string sessionId", async () => {
115+
await handler.handle(
116+
{ command: "terminal-session-history", sessionId: 123 } as any,
117+
ctx,
118+
);
119+
120+
assert.ok(postMessageStub.calledOnce);
121+
const msg = postMessageStub.firstCall.args[0];
122+
assert.strictEqual(msg.type, "terminal-error");
123+
});
124+
125+
// ── terminal-session-output ────────────────────────────────────
126+
127+
test("output returns new data for valid session", async () => {
128+
serviceStub.readOutput.returns("new chunk");
129+
130+
await handler.handle(
131+
{ command: "terminal-session-output", sessionId: "s1" },
132+
ctx,
133+
);
134+
135+
assert.ok(postMessageStub.calledOnce);
136+
const msg = postMessageStub.firstCall.args[0];
137+
assert.strictEqual(msg.type, "terminal-session-output-result");
138+
assert.strictEqual(msg.sessionId, "s1");
139+
assert.strictEqual(msg.output, "new chunk");
140+
});
141+
142+
test("output returns error for missing session", async () => {
143+
serviceStub.readOutput.returns(null);
144+
145+
await handler.handle(
146+
{ command: "terminal-session-output", sessionId: "gone" },
147+
ctx,
148+
);
149+
150+
assert.ok(postMessageStub.calledOnce);
151+
const msg = postMessageStub.firstCall.args[0];
152+
assert.strictEqual(msg.type, "terminal-error");
153+
assert.strictEqual(msg.sessionId, "gone");
154+
assert.ok(msg.error.includes("not found"));
155+
});
156+
157+
test("output returns error for empty sessionId", async () => {
158+
await handler.handle(
159+
{ command: "terminal-session-output", sessionId: "" },
160+
ctx,
161+
);
162+
163+
assert.ok(postMessageStub.calledOnce);
164+
const msg = postMessageStub.firstCall.args[0];
165+
assert.strictEqual(msg.type, "terminal-error");
166+
assert.strictEqual(msg.sessionId, null);
167+
});
168+
169+
// ── unknown / malformed ────────────────────────────────────────
170+
171+
test("ignores unrelated commands", async () => {
172+
await handler.handle({ command: "some-other-command" }, ctx);
173+
assert.ok(postMessageStub.notCalled);
174+
});
175+
176+
test("ignores malformed messages", async () => {
177+
await handler.handle({} as any, ctx);
178+
assert.ok(postMessageStub.notCalled);
179+
180+
await handler.handle({ command: 123 } as any, ctx);
181+
assert.ok(postMessageStub.notCalled);
182+
});
183+
});

src/webview-providers/base.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,8 @@ import { type ProviderKey, toProviderKey } from "./provider-name";
5454
import {
5555
BrowserHandler,
5656
ComposerHandler,
57-
ConnectorHandler,
5857
CostTrackingHandler,
58+
TerminalViewerHandler,
5959
SkillHandler,
6060
DiffReviewHandler,
6161
CheckpointHandler,
@@ -241,7 +241,6 @@ export abstract class BaseWebViewProvider implements vscode.Disposable {
241241
);
242242
this.handlerRegistry.register(new DockerHandler());
243243
this.handlerRegistry.register(new MCPHandler());
244-
this.handlerRegistry.register(new ConnectorHandler());
245244
this.handlerRegistry.register(new SkillHandler());
246245
this.handlerRegistry.register(
247246
new NewsHandler(() => this.synchronizeNews()),
@@ -272,6 +271,7 @@ export abstract class BaseWebViewProvider implements vscode.Disposable {
272271
this.handlerRegistry.register(new StandupHandler());
273272
this.handlerRegistry.register(new TeamGraphHandler());
274273
this.handlerRegistry.register(new CostTrackingHandler());
274+
this.handlerRegistry.register(new TerminalViewerHandler());
275275
this.handlerRegistry.register(new DoctorHandler());
276276
this.handlerRegistry.register(new OnboardingHandler());
277277
this.handlerRegistry.register(

src/webview-providers/handlers/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ export {
66
export { SettingsHandler } from "./settings-handler";
77
export { DockerHandler } from "./docker-handler";
88
export { MCPHandler } from "./mcp-handler";
9-
export { ConnectorHandler } from "./connector-handler";
109
export { SkillHandler } from "./skill-handler";
1110
export { NewsHandler } from "./news-handler";
1211
export { BrowserHandler } from "./browser-handler";
@@ -21,5 +20,6 @@ export { ComposerHandler } from "./composer-handler";
2120
export { StandupHandler } from "./standup-handler";
2221
export { TeamGraphHandler } from "./team-graph-handler";
2322
export { CostTrackingHandler } from "./cost-tracking-handler";
23+
export { TerminalViewerHandler } from "./terminal-viewer-handler";
2424
export { DoctorHandler } from "./doctor-handler";
2525
export { OnboardingHandler } from "./onboarding-handler";

src/webview-providers/handlers/onboarding-handler.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ function isOnboardingMessage(msg: unknown): msg is OnboardingMessage {
6666

6767
interface OnboardingStateDTO {
6868
shouldShow: boolean;
69+
completed: boolean;
6970
providers: Array<{
7071
id: string;
7172
name: string;
@@ -94,16 +95,14 @@ export class OnboardingHandler implements WebviewMessageHandler {
9495
try {
9596
const shouldShow = svc.shouldShowOnboarding();
9697
const providers = svc.getProviders();
97-
let projectInfo: ProjectInfo | null = null;
98-
let suggestedTasks: Array<{ label: string; prompt: string }> = [];
9998

100-
if (shouldShow) {
101-
projectInfo = await svc.detectProjectInfo();
102-
suggestedTasks = svc.getSuggestedTasks(projectInfo);
103-
}
99+
// Always detect project info so the WelcomeScreen can show it
100+
const projectInfo = await svc.detectProjectInfo();
101+
const suggestedTasks = svc.getSuggestedTasks(projectInfo);
104102

105103
const dto: OnboardingStateDTO = {
106104
shouldShow,
105+
completed: !shouldShow,
107106
providers,
108107
projectInfo,
109108
suggestedTasks,
@@ -121,6 +120,7 @@ export class OnboardingHandler implements WebviewMessageHandler {
121120
command: "onboarding-state",
122121
data: {
123122
shouldShow: false,
123+
completed: false,
124124
providers: [],
125125
projectInfo: null,
126126
suggestedTasks: [],

0 commit comments

Comments
 (0)