@gajae-code/coding-agent 0.9.2 → 0.9.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/dist/types/build-metadata.d.ts +7 -0
  3. package/dist/types/capability/mcp.d.ts +3 -1
  4. package/dist/types/commands/mcp.d.ts +1 -1
  5. package/dist/types/config/keybindings.d.ts +2 -2
  6. package/dist/types/modes/components/hook-selector.d.ts +1 -0
  7. package/dist/types/modes/components/status-line/types.d.ts +6 -0
  8. package/dist/types/modes/components/status-line.d.ts +1 -0
  9. package/dist/types/modes/components/welcome.d.ts +1 -0
  10. package/dist/types/modes/rpc/rpc-client.d.ts +7 -2
  11. package/dist/types/modes/utils/ui-helpers.d.ts +12 -0
  12. package/dist/types/notifications/telegram-daemon.d.ts +2 -0
  13. package/dist/types/runtime-mcp/config-writer.d.ts +4 -3
  14. package/dist/types/runtime-mcp/types.d.ts +5 -4
  15. package/dist/types/session/agent-session.d.ts +16 -0
  16. package/dist/types/session/session-manager.d.ts +1 -0
  17. package/dist/types/tools/skill.d.ts +9 -8
  18. package/dist/types/utils/linkedom.d.ts +4 -0
  19. package/dist/types/utils/pasted-image-path.d.ts +5 -3
  20. package/package.json +11 -7
  21. package/scripts/build-binary.ts +2 -40
  22. package/scripts/compile-args.ts +96 -0
  23. package/scripts/generate-docs-index.ts +1 -0
  24. package/src/build-metadata.ts +104 -0
  25. package/src/capability/mcp.ts +3 -1
  26. package/src/cli/mcp-cli.ts +108 -26
  27. package/src/cli.ts +21 -0
  28. package/src/commands/mcp.ts +8 -8
  29. package/src/commands/stats.ts +6 -2
  30. package/src/config/keybindings.ts +2 -2
  31. package/src/config/mcp-schema.json +21 -17
  32. package/src/coordinator-mcp/server.ts +7 -4
  33. package/src/discovery/builtin.ts +14 -0
  34. package/src/discovery/mcp-json.ts +14 -0
  35. package/src/eval/js/executor.ts +7 -1
  36. package/src/gjc-runtime/state-runtime.ts +105 -12
  37. package/src/gjc-runtime/workflow-manifest.generated.json +4 -28
  38. package/src/gjc-runtime/workflow-manifest.ts +1 -7
  39. package/src/internal-urls/docs-index.generated.ts +3 -2
  40. package/src/internal-urls/gjc-protocol.ts +14 -1
  41. package/src/modes/components/assistant-message.ts +1 -1
  42. package/src/modes/components/hook-selector.ts +4 -0
  43. package/src/modes/components/session-selector.ts +2 -2
  44. package/src/modes/components/status-line/presets.ts +5 -7
  45. package/src/modes/components/status-line/segments.ts +14 -0
  46. package/src/modes/components/status-line/types.ts +6 -0
  47. package/src/modes/components/status-line.ts +13 -6
  48. package/src/modes/components/tool-execution.ts +1 -1
  49. package/src/modes/components/welcome.ts +4 -1
  50. package/src/modes/controllers/command-controller.ts +1 -1
  51. package/src/modes/controllers/event-controller.ts +9 -9
  52. package/src/modes/controllers/input-controller.ts +8 -2
  53. package/src/modes/controllers/selector-controller.ts +7 -3
  54. package/src/modes/interactive-mode.ts +12 -4
  55. package/src/modes/rpc/rpc-client.ts +30 -6
  56. package/src/modes/utils/ui-helpers.ts +154 -32
  57. package/src/notifications/telegram-daemon.ts +28 -2
  58. package/src/prompts/tools/skill.md +2 -2
  59. package/src/runtime-mcp/config-writer.ts +4 -3
  60. package/src/runtime-mcp/config.ts +1 -0
  61. package/src/runtime-mcp/types.ts +5 -4
  62. package/src/session/agent-session.ts +117 -3
  63. package/src/session/session-manager.ts +68 -40
  64. package/src/slash-commands/builtin-registry.ts +9 -4
  65. package/src/tools/bash-interactive.ts +9 -4
  66. package/src/tools/browser/readable.ts +2 -2
  67. package/src/tools/fetch.ts +5 -5
  68. package/src/tools/skill.ts +14 -11
  69. package/src/utils/linkedom.ts +14 -0
  70. package/src/utils/pasted-image-path.ts +23 -9
  71. package/src/web/scrapers/arxiv.ts +2 -2
  72. package/src/web/scrapers/go-pkg.ts +2 -2
  73. package/src/web/scrapers/iacr.ts +2 -2
  74. package/src/web/scrapers/readthedocs.ts +2 -2
  75. package/src/web/scrapers/twitter.ts +3 -2
  76. package/src/web/scrapers/wikipedia.ts +2 -2
  77. package/src/export/html/template.css +0 -1060
  78. package/src/export/html/template.html +0 -47
  79. package/src/export/html/template.js +0 -2314
  80. package/vendor/insane-search/engine/tests/test_hardening.py +0 -57
  81. package/vendor/insane-search/engine/tests/test_smoke.py +0 -152
  82. package/vendor/insane-search/engine/tests/test_u1.py +0 -200
  83. package/vendor/insane-search/engine/tests/test_u4.py +0 -131
  84. package/vendor/insane-search/engine/tests/test_u5.py +0 -163
  85. package/vendor/insane-search/engine/tests/test_u7.py +0 -124
  86. package/vendor/insane-search/engine/tests/test_u8.py +0 -216
@@ -29,7 +29,7 @@ import { calculateTokensPerSecond } from "./status-line/token-rate";
29
29
  import type { SeparatorDef } from "./status-line/types";
30
30
 
31
31
  export interface StatusLineSegmentOptions {
32
- model?: { showThinkingLevel?: boolean };
32
+ model?: { showThinkingLevel?: boolean; showContextPercent?: boolean };
33
33
  path?: { abbreviate?: boolean; maxLength?: number; stripWorkPrefix?: boolean };
34
34
  git?: { showBranch?: boolean; showStaged?: boolean; showUnstaged?: boolean; showUntracked?: boolean };
35
35
  time?: { format?: "12h" | "24h"; showSeconds?: boolean };
@@ -700,6 +700,11 @@ export class StatusLineComponent implements Component {
700
700
  const contextTokens = breakdown.usedTokens;
701
701
  const contextWindow = breakdown.contextWindow || state.model?.contextWindow || 0;
702
702
  const contextPercent = contextWindow > 0 ? (contextTokens / contextWindow) * 100 : 0;
703
+ // Suppress the inline model percentage when a standalone context_pct
704
+ // segment is also rendered, so the value is not shown twice.
705
+ const contextPctSegmentActive =
706
+ effectiveSettings.leftSegments.includes("context_pct") ||
707
+ effectiveSettings.rightSegments.includes("context_pct");
703
708
 
704
709
  return {
705
710
  session: this.session,
@@ -710,6 +715,7 @@ export class StatusLineComponent implements Component {
710
715
  usageStats,
711
716
  contextPercent,
712
717
  contextWindow,
718
+ contextPctSegmentActive,
713
719
  autoCompactEnabled: this.#autoCompactEnabled,
714
720
  subagentCount: this.#subagentCount,
715
721
  jobs: this.#jobs,
@@ -923,11 +929,8 @@ export class StatusLineComponent implements Component {
923
929
  const totalWidth = () => leftWidth + rightWidth + (left.length > 0 && right.length > 0 ? 1 : 0);
924
930
 
925
931
  if (topFillWidth > 0) {
926
- while (totalWidth() > topFillWidth && right.length > 0) {
927
- right.pop();
928
- rightWidth = this.#groupWidth(right, rightCapWidth, rightSepWidth);
929
- }
930
- // Shrink path before dropping left segments — path is the only elastic segment
932
+ // Shrink path before dropping right-side telemetry — path is the only elastic segment,
933
+ // and presets such as default-usage should not hide usage just because cwd is long.
931
934
  const pathIdx = leftIds.indexOf("path");
932
935
  if (pathIdx >= 0 && totalWidth() > topFillWidth) {
933
936
  const overflow = totalWidth() - topFillWidth;
@@ -937,6 +940,10 @@ export class StatusLineComponent implements Component {
937
940
  leftWidth = this.#groupWidth(left, leftCapWidth, leftSepWidth);
938
941
  }
939
942
  }
943
+ while (totalWidth() > topFillWidth && right.length > 0) {
944
+ right.pop();
945
+ rightWidth = this.#groupWidth(right, rightCapWidth, rightSepWidth);
946
+ }
940
947
  while (totalWidth() > topFillWidth && left.length > 0) {
941
948
  left.pop();
942
949
  leftIds.pop();
@@ -678,7 +678,7 @@ export class ToolExecutionComponent extends Container {
678
678
  imageData,
679
679
  imageMimeType,
680
680
  { fallbackColor: (s: string) => theme.fg("toolOutput", s) },
681
- resolveImageOptions(),
681
+ { ...resolveImageOptions(), refetch: () => imageData },
682
682
  );
683
683
  this.#imageComponents.push(imageComponent);
684
684
  this.addChild(imageComponent);
@@ -1,5 +1,6 @@
1
1
  import { type Component, padding, TERMINAL, truncateToWidth, visibleWidth } from "@gajae-code/tui";
2
2
  import { APP_NAME } from "@gajae-code/utils";
3
+ import { formatBuildLabel } from "../../build-metadata";
3
4
  import { type ThemeColor, theme } from "../../modes/theme/theme";
4
5
 
5
6
  export interface RecentSession {
@@ -19,6 +20,7 @@ export interface WelcomeComponentOptions {
19
20
  getReservedBottomRows?: (termWidth: number) => number;
20
21
  changelogMarkdown?: string;
21
22
  collapseChangelog?: boolean;
23
+ buildLabel?: string;
22
24
  }
23
25
 
24
26
  const WELCOME_STATIC_RIGHT_ROWS_EXCLUDING_DYNAMIC_SECTIONS = 9;
@@ -227,7 +229,8 @@ export class WelcomeComponent implements Component {
227
229
  const br = theme.fg("dim", theme.boxRound.bottomRight);
228
230
 
229
231
  const lines: string[] = [];
230
- const title = ` ${APP_NAME} v${this.version} · GJC Forge `;
232
+ const buildLabel = this.options.buildLabel ?? formatBuildLabel();
233
+ const title = ` ${APP_NAME} v${this.version} · ${buildLabel} · GJC Forge `;
231
234
  const titlePrefixRaw = hChar.repeat(3);
232
235
  const titleStyled = theme.fg("dim", titlePrefixRaw) + theme.fg("muted", title);
233
236
  const titleVisLen = visibleWidth(titlePrefixRaw) + visibleWidth(title);
@@ -567,7 +567,7 @@ export class CommandController {
567
567
  `| Start a fresh session | \`${sessionNewKey}\` or \`/new\` |`,
568
568
  "| Resume another session | `/resume` |",
569
569
  "| Show session details | `/session info` |",
570
- "| Delete current session | `/session delete` |",
570
+ "| Delete current session transcript/artifacts | `/session delete` |",
571
571
  "| Select a model | `/model` or `Ctrl+L` |",
572
572
  "| Show all shortcuts | `?` on an empty prompt or `/hotkeys` |",
573
573
  "",
@@ -26,7 +26,7 @@ import { interruptHint } from "../shared";
26
26
  import { buildAbortDisplayMessage } from "../utils/abort-message";
27
27
  import { consumeInjectedOptimisticSignature } from "../utils/injected-user-submission";
28
28
  import { ringTerminalBell } from "../utils/terminal-bell";
29
- import { argsWithPartialJson } from "../utils/ui-helpers";
29
+ import { addChatChild, argsWithPartialJson } from "../utils/ui-helpers";
30
30
 
31
31
  type AgentSessionEventKind = AgentSessionEvent["type"];
32
32
 
@@ -169,12 +169,12 @@ export class EventController {
169
169
 
170
170
  #getReadGroup(): ReadToolGroupComponent {
171
171
  if (!this.#lastReadGroup) {
172
- this.ctx.chatContainer.addChild(new Text("", 0, 0));
172
+ addChatChild(this.ctx, new Text("", 0, 0));
173
173
  const group = new ReadToolGroupComponent({
174
174
  showContentPreview: this.ctx.settings.get("read.toolResultPreview"),
175
175
  });
176
176
  group.setExpanded(this.ctx.toolOutputExpanded);
177
- this.ctx.chatContainer.addChild(group);
177
+ addChatChild(this.ctx, group);
178
178
  this.#lastReadGroup = group;
179
179
  }
180
180
  return this.#lastReadGroup;
@@ -332,7 +332,7 @@ export class EventController {
332
332
  this.ctx.ui.requestRender(),
333
333
  );
334
334
  this.ctx.streamingMessage = event.message;
335
- this.ctx.chatContainer.addChild(this.ctx.streamingComponent);
335
+ addChatChild(this.ctx, this.ctx.streamingComponent);
336
336
  this.ctx.streamingComponent.updateContent(this.ctx.streamingMessage, { streaming: true });
337
337
  this.ctx.ui.requestRender();
338
338
  }
@@ -445,7 +445,7 @@ export class EventController {
445
445
  );
446
446
  if (!this.ctx.pendingTools.has(content.id)) {
447
447
  this.#resetReadGroup();
448
- this.ctx.chatContainer.addChild(new Text("", 0, 0));
448
+ addChatChild(this.ctx, new Text("", 0, 0));
449
449
  const tool = this.ctx.session.getToolByName(content.name);
450
450
  const component = new ToolExecutionComponent(
451
451
  content.name,
@@ -462,8 +462,8 @@ export class EventController {
462
462
  content.id,
463
463
  );
464
464
  component.setExpanded(this.ctx.toolOutputExpanded);
465
- this.ctx.chatContainer.addChild(component);
466
465
  this.ctx.pendingTools.set(content.id, component);
466
+ addChatChild(this.ctx, component);
467
467
  } else {
468
468
  this.ctx.pendingTools.get(content.id)?.updateArgs(renderArgs, content.id);
469
469
  }
@@ -575,8 +575,8 @@ export class EventController {
575
575
  event.toolCallId,
576
576
  );
577
577
  component.setExpanded(this.ctx.toolOutputExpanded);
578
- this.ctx.chatContainer.addChild(component);
579
578
  this.ctx.pendingTools.set(event.toolCallId, component);
579
+ addChatChild(this.ctx, component);
580
580
  this.ctx.ui.requestRender();
581
581
  }
582
582
  }
@@ -869,13 +869,13 @@ export class EventController {
869
869
  async #handleTtsrTriggered(event: Extract<AgentSessionEvent, { type: "ttsr_triggered" }>): Promise<void> {
870
870
  const component = new TtsrNotificationComponent(event.rules);
871
871
  component.setExpanded(this.ctx.toolOutputExpanded);
872
- this.ctx.chatContainer.addChild(component);
872
+ addChatChild(this.ctx, component);
873
873
  this.ctx.ui.requestRender();
874
874
  }
875
875
 
876
876
  async #handleTodoReminder(event: Extract<AgentSessionEvent, { type: "todo_reminder" }>): Promise<void> {
877
877
  const component = new TodoReminderComponent(event.todos, event.attempt, event.maxAttempts);
878
- this.ctx.chatContainer.addChild(component);
878
+ addChatChild(this.ctx, component);
879
879
  this.ctx.ui.requestRender();
880
880
  }
881
881
 
@@ -18,7 +18,7 @@ import { copyToClipboard, readImageFromClipboard } from "../../utils/clipboard";
18
18
  import { getEditorCommand, openInEditor } from "../../utils/external-editor";
19
19
  import { ensureSupportedImageInput, ImageInputTooLargeError, loadImageInput } from "../../utils/image-loading";
20
20
  import { resizeImage } from "../../utils/image-resize";
21
- import { resolvePastedImagePath } from "../../utils/pasted-image-path";
21
+ import { formatPastedImageReference, resolvePastedImagePath } from "../../utils/pasted-image-path";
22
22
  import { generateSessionTitle, setSessionTerminalTitle } from "../../utils/title-generator";
23
23
  import { type QueuedMessageMoveDirection, QueuedMessageSelectorComponent } from "../components/queued-message-selector";
24
24
 
@@ -149,6 +149,12 @@ export class InputController {
149
149
  return { consume: true };
150
150
  }
151
151
  const hookDialogActive = this.#hasHookDialog();
152
+ if (this.ctx.hookSelector?.hasActiveInlineInput?.() === true) {
153
+ // Inline ask/custom-input editors use Esc to return to the option list.
154
+ // Let the focused selector see the key instead of converting a typo
155
+ // into a full workflow/session abort while the agent is streaming.
156
+ return undefined;
157
+ }
152
158
  if (
153
159
  this.#handleCancellableWorkEscape({
154
160
  loading: hookDialogActive,
@@ -1096,7 +1102,7 @@ export class InputController {
1096
1102
  data: image.data,
1097
1103
  mimeType: image.mimeType,
1098
1104
  });
1099
- this.ctx.editor.insertText(`${this.#nextImagePlaceholder()} `);
1105
+ this.ctx.editor.insertText(`${formatPastedImageReference(this.#nextImagePlaceholder(), image.resolvedPath)} `);
1100
1106
  this.ctx.showStatus(`Attached image: ${path.basename(image.resolvedPath)}`, { dim: true });
1101
1107
  this.ctx.ui.requestRender();
1102
1108
  return true;
@@ -1431,8 +1431,12 @@ export class SelectorController {
1431
1431
  }
1432
1432
 
1433
1433
  const confirmed = await this.ctx.showHookConfirm(
1434
- "Delete Session",
1435
- "This will permanently delete the current session.\nYou will be returned to the session selector.",
1434
+ "Delete current session transcript and artifacts?",
1435
+ [
1436
+ "This permanently deletes only the current session transcript file and its artifacts directory.",
1437
+ "Other sessions and topic/history metadata are not deleted.",
1438
+ "You will be moved to a fresh session and returned to the session selector.",
1439
+ ].join("\n"),
1436
1440
  );
1437
1441
 
1438
1442
  if (!confirmed) {
@@ -1449,7 +1453,7 @@ export class SelectorController {
1449
1453
  await storage.deleteSessionWithArtifacts(sessionFile);
1450
1454
 
1451
1455
  // Show session selector
1452
- this.ctx.showStatus("Session deleted");
1456
+ this.ctx.showStatus("Current session transcript and artifacts deleted");
1453
1457
  await this.showSessionSelector();
1454
1458
  }
1455
1459
 
@@ -18,6 +18,7 @@ import type { Component, EditorTheme, SlashCommand } from "@gajae-code/tui";
18
18
  import {
19
19
  Container,
20
20
  clearRenderCache,
21
+ getRenderCacheRetainedBytes,
21
22
  Loader,
22
23
  Markdown,
23
24
  ProcessTerminal,
@@ -114,7 +115,7 @@ import {
114
115
  theme,
115
116
  } from "./theme/theme";
116
117
  import type { CompactionQueuedMessage, InteractiveModeContext, SubmittedUserInput, TodoItem, TodoPhase } from "./types";
117
- import { UiHelpers } from "./utils/ui-helpers";
118
+ import { addChatChild, UiHelpers } from "./utils/ui-helpers";
118
119
 
119
120
  const INTERACTIVE_ABORT_CLEANUP_TIMEOUT_MS = 5_000;
120
121
  const COMPOSER_NEWLINE_HINT = process.platform === "win32" ? "Alt+Enter/Ctrl+J" : "Shift+Enter/Ctrl+J";
@@ -398,6 +399,10 @@ export class InteractiveMode implements InteractiveModeContext {
398
399
  ) {
399
400
  this.session = session;
400
401
  this.sessionManager = session.sessionManager;
402
+ this.session.setRetainedMemorySampler(() => ({
403
+ tuiChatChildren: this.chatContainer.children.length,
404
+ tuiCachedRenderBytes: getRenderCacheRetainedBytes(),
405
+ }));
401
406
  this.settings = session.settings;
402
407
  this.keybindings = KeybindingsManager.inMemory();
403
408
  this.agent = session.agent;
@@ -1514,8 +1519,11 @@ export class InteractiveMode implements InteractiveModeContext {
1514
1519
 
1515
1520
  #renderPlanPreview(planContent: string, options?: { append?: boolean }): void {
1516
1521
  const existingContainer = this.#planReviewContainer;
1517
- const replaceExisting = options?.append !== true && existingContainer !== undefined;
1518
- const planReviewContainer = replaceExisting ? existingContainer : new Container();
1522
+ // Only reuse the existing preview when it is still attached to the chat container;
1523
+ // a collapsed/disposed container must be re-added, not mutated in place.
1524
+ const stillAttached = existingContainer !== undefined && this.chatContainer.children.includes(existingContainer);
1525
+ const replaceExisting = options?.append !== true && stillAttached;
1526
+ const planReviewContainer = replaceExisting && existingContainer ? existingContainer : new Container();
1519
1527
  planReviewContainer.clear();
1520
1528
  planReviewContainer.addChild(new Spacer(1));
1521
1529
  planReviewContainer.addChild(new DynamicBorder());
@@ -1524,7 +1532,7 @@ export class InteractiveMode implements InteractiveModeContext {
1524
1532
  planReviewContainer.addChild(new Markdown(planContent, 1, 1, getMarkdownTheme()));
1525
1533
  planReviewContainer.addChild(new DynamicBorder());
1526
1534
  if (!replaceExisting) {
1527
- this.chatContainer.addChild(planReviewContainer);
1535
+ addChatChild(this, planReviewContainer);
1528
1536
  }
1529
1537
  this.#planReviewContainer = planReviewContainer;
1530
1538
  this.ui.requestRender();
@@ -8,7 +8,8 @@ import type { CompactionResult } from "@gajae-code/agent-core/compaction";
8
8
  import type { ImageContent, Model } from "@gajae-code/ai";
9
9
  import { isRecord, ptree, readJsonl } from "@gajae-code/utils";
10
10
  import type { BashResult } from "../../exec/bash-executor";
11
- import type { SessionStats } from "../../session/agent-session";
11
+ import type { AgentSessionEvent, SessionStats } from "../../session/agent-session";
12
+ import { AGENT_WIRE_EVENT_TYPES, type AgentWireEventType } from "../shared/agent-wire/event-contract";
12
13
  import type {
13
14
  RpcCommand,
14
15
  RpcExtensionUIRequest,
@@ -64,6 +65,7 @@ export interface RpcClientOptions {
64
65
  export type ModelInfo = Pick<Model, "provider" | "id" | "contextWindow" | "reasoning" | "thinking">;
65
66
 
66
67
  export type RpcEventListener = (event: AgentEvent) => void;
68
+ export type RpcSessionEventListener = (event: AgentSessionEvent) => void;
67
69
 
68
70
  export interface RpcClientToolContext<TDetails = unknown> {
69
71
  toolCallId: string;
@@ -91,7 +93,8 @@ export function defineRpcClientTool<
91
93
  return tool;
92
94
  }
93
95
 
94
- const agentEventTypes = new Set<AgentEvent["type"]>([
96
+ const agentWireEventTypes = new Set<AgentWireEventType>(AGENT_WIRE_EVENT_TYPES);
97
+ const coreAgentEventTypes = new Set<AgentEvent["type"]>([
95
98
  "agent_start",
96
99
  "agent_end",
97
100
  "turn_start",
@@ -116,13 +119,16 @@ function isRpcResponse(value: unknown): value is RpcResponse {
116
119
  return true;
117
120
  }
118
121
 
119
- function isAgentEvent(value: unknown): value is AgentEvent {
122
+ function isAgentSessionEvent(value: unknown): value is AgentSessionEvent {
120
123
  if (!isRecord(value)) return false;
121
124
  const type = value.type;
122
125
  if (typeof type !== "string") return false;
123
- return agentEventTypes.has(type as AgentEvent["type"]);
126
+ return agentWireEventTypes.has(type as AgentWireEventType);
124
127
  }
125
128
 
129
+ function isCoreAgentEvent(value: AgentSessionEvent): value is AgentEvent {
130
+ return coreAgentEventTypes.has(value.type as AgentEvent["type"]);
131
+ }
126
132
  function isRpcHostToolCallRequest(value: unknown): value is RpcHostToolCallRequest {
127
133
  if (!isRecord(value)) return false;
128
134
  return (
@@ -378,6 +384,7 @@ export class RpcClient {
378
384
  #transport: RpcTransport | null = null;
379
385
  #transportClosed = false;
380
386
  #eventListeners: RpcEventListener[] = [];
387
+ #sessionEventListeners: RpcSessionEventListener[] = [];
381
388
  #pendingRequests: Map<string, { resolve: (response: RpcResponse) => void; reject: (error: Error) => void }> =
382
389
  new Map();
383
390
  #customTools: RpcClientCustomTool[] = [];
@@ -442,7 +449,7 @@ export class RpcClient {
442
449
  }
443
450
 
444
451
  /**
445
- * Subscribe to agent events.
452
+ * Subscribe to the legacy core AgentEvent subset.
446
453
  */
447
454
  onEvent(listener: RpcEventListener): () => void {
448
455
  this.#eventListeners.push(listener);
@@ -454,6 +461,19 @@ export class RpcClient {
454
461
  };
455
462
  }
456
463
 
464
+ /**
465
+ * Subscribe to all renderer-facing agent session events.
466
+ */
467
+ onSessionEvent(listener: RpcSessionEventListener): () => void {
468
+ this.#sessionEventListeners.push(listener);
469
+ return () => {
470
+ const index = this.#sessionEventListeners.indexOf(listener);
471
+ if (index !== -1) {
472
+ this.#sessionEventListeners.splice(index, 1);
473
+ }
474
+ };
475
+ }
476
+
457
477
  /**
458
478
  * Subscribe to workflow lifecycle gates emitted by RPC mode.
459
479
  */
@@ -928,8 +948,12 @@ export class RpcClient {
928
948
 
929
949
  // Canonical agent-wire event frame: { type:"event", payload:{ event_type, event } }.
930
950
  const event = unwrapAgentWireEventFrame(data);
931
- if (!isAgentEvent(event)) return;
951
+ if (!isAgentSessionEvent(event)) return;
932
952
 
953
+ for (const listener of this.#sessionEventListeners) {
954
+ listener(event);
955
+ }
956
+ if (!isCoreAgentEvent(event)) return;
933
957
  for (const listener of this.#eventListeners) {
934
958
  listener(event);
935
959
  }