@gajae-code/coding-agent 0.16.1 → 0.16.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 (146) hide show
  1. package/CHANGELOG.md +70 -0
  2. package/dist/types/cli/args.d.ts +2 -2
  3. package/dist/types/cli/root-flags.d.ts +4 -2
  4. package/dist/types/cli.d.ts +1 -1
  5. package/dist/types/commands/launch.d.ts +1 -1
  6. package/dist/types/config/autorouting-tier-map.d.ts +3 -0
  7. package/dist/types/config/model-registry.d.ts +4 -0
  8. package/dist/types/config/model-resolver.d.ts +9 -0
  9. package/dist/types/config/settings-schema.d.ts +29 -1
  10. package/dist/types/discovery/claude-plugins.d.ts +3 -1
  11. package/dist/types/discovery/helpers.d.ts +2 -0
  12. package/dist/types/extensibility/gjc-plugins/mcp-policy.d.ts +16 -0
  13. package/dist/types/extensibility/gjc-plugins/paths.d.ts +2 -0
  14. package/dist/types/extensibility/runtime-skill-discovery.d.ts +1 -1
  15. package/dist/types/extensibility/shared-events.d.ts +1 -1
  16. package/dist/types/gjc-runtime/session-state-lock.d.ts +2 -0
  17. package/dist/types/modes/components/assistant-message.d.ts +1 -2
  18. package/dist/types/modes/components/settings-selector.d.ts +6 -1
  19. package/dist/types/modes/components/status-line/command.d.ts +35 -0
  20. package/dist/types/modes/components/status-line/index.d.ts +1 -0
  21. package/dist/types/modes/components/status-line/types.d.ts +5 -0
  22. package/dist/types/modes/components/tool-status-header.d.ts +14 -0
  23. package/dist/types/modes/controllers/event-controller.d.ts +6 -0
  24. package/dist/types/modes/interactive-mode.d.ts +1 -0
  25. package/dist/types/modes/types.d.ts +1 -0
  26. package/dist/types/runtime-mcp/client.d.ts +9 -1
  27. package/dist/types/runtime-mcp/manager.d.ts +7 -1
  28. package/dist/types/runtime-mcp/pool.d.ts +22 -3
  29. package/dist/types/runtime-mcp/transports/stdio.d.ts +0 -4
  30. package/dist/types/runtime-mcp/types.d.ts +22 -0
  31. package/dist/types/sdk/bus/chat-daemon-runtime.d.ts +12 -0
  32. package/dist/types/sdk/bus/slack-live-provider.d.ts +6 -0
  33. package/dist/types/sdk/bus/slack-provider.d.ts +12 -0
  34. package/dist/types/sdk/cli/rows.d.ts +7 -0
  35. package/dist/types/sdk/host/events.d.ts +1 -0
  36. package/dist/types/sdk/host/host.d.ts +2 -0
  37. package/dist/types/sdk/host/session-runtime.d.ts +23 -0
  38. package/dist/types/sdk/session-api-key.d.ts +26 -0
  39. package/dist/types/sdk/transport/serve-cli.d.ts +1 -1
  40. package/dist/types/session/agent-session.d.ts +9 -0
  41. package/dist/types/setup/paseo/announce.d.ts +111 -0
  42. package/dist/types/setup/paseo/setup-deps.d.ts +2 -1
  43. package/dist/types/system-prompt.d.ts +2 -0
  44. package/dist/types/task/executor.d.ts +2 -1
  45. package/dist/types/tools/fetch.d.ts +7 -0
  46. package/dist/types/tools/index.d.ts +8 -2
  47. package/dist/types/tools/path-utils.d.ts +2 -0
  48. package/dist/types/tools/search.d.ts +6 -0
  49. package/dist/types/utils/external-editor.d.ts +5 -0
  50. package/package.json +7 -7
  51. package/scripts/capture-status-line-custom-editor-showcase.ts +151 -0
  52. package/scripts/generate-sdk-operation-inventory.ts +9 -0
  53. package/scripts/verify-status-line-custom-editor-showcase.ts +309 -0
  54. package/src/cli/args.ts +20 -9
  55. package/src/cli/root-flags.ts +5 -3
  56. package/src/config/autorouting-tier-map.ts +1 -0
  57. package/src/config/file-lock.ts +181 -17
  58. package/src/config/model-preset-registry.ts +5 -3
  59. package/src/config/model-profiles.ts +40 -0
  60. package/src/config/model-registry.ts +57 -11
  61. package/src/config/model-resolver.ts +48 -0
  62. package/src/config/settings-schema.ts +33 -1
  63. package/src/config/settings.ts +1 -0
  64. package/src/defaults/gjc/extensions/grok-cli-vendor/src/models/catalog.ts +11 -11
  65. package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/register.ts +13 -2
  66. package/src/discovery/builtin.ts +13 -4
  67. package/src/discovery/claude-plugins.ts +12 -4
  68. package/src/discovery/helpers.ts +10 -2
  69. package/src/extensibility/gjc-plugins/compiler.ts +208 -58
  70. package/src/extensibility/gjc-plugins/installer.ts +13 -12
  71. package/src/extensibility/gjc-plugins/mcp-policy.ts +81 -52
  72. package/src/extensibility/gjc-plugins/paths.ts +13 -0
  73. package/src/extensibility/gjc-plugins/runtime-adapters.ts +758 -16
  74. package/src/extensibility/runtime-skill-discovery.ts +77 -151
  75. package/src/extensibility/shared-events.ts +1 -1
  76. package/src/extensibility/skills.ts +44 -16
  77. package/src/gjc-runtime/repository-binding.ts +1 -1
  78. package/src/gjc-runtime/session-state-lock.ts +94 -6
  79. package/src/internal-urls/docs-index.generated.ts +10 -10
  80. package/src/main.ts +72 -34
  81. package/src/modes/DESIGN.md +54 -0
  82. package/src/modes/acp/acp-agent.ts +29 -14
  83. package/src/modes/components/assistant-message.ts +22 -9
  84. package/src/modes/components/model-selector.ts +166 -33
  85. package/src/modes/components/settings-selector.ts +1075 -235
  86. package/src/modes/components/status-line/command.ts +190 -0
  87. package/src/modes/components/status-line/index.ts +1 -0
  88. package/src/modes/components/status-line/segments.ts +32 -5
  89. package/src/modes/components/status-line/types.ts +5 -0
  90. package/src/modes/components/tool-status-header.ts +393 -21
  91. package/src/modes/controllers/command-controller.ts +2 -0
  92. package/src/modes/controllers/event-controller.ts +148 -5
  93. package/src/modes/controllers/selector-controller.ts +21 -0
  94. package/src/modes/interactive-mode.ts +47 -1
  95. package/src/modes/types.ts +1 -0
  96. package/src/modes/utils/ui-helpers.ts +1 -0
  97. package/src/prompts/system/system-prompt.md +8 -0
  98. package/src/prompts/tools/bash.md +1 -1
  99. package/src/prompts/tools/job.md +1 -1
  100. package/src/prompts/tools/search.md +2 -0
  101. package/src/runtime-mcp/client.ts +60 -17
  102. package/src/runtime-mcp/manager.ts +171 -14
  103. package/src/runtime-mcp/pool.test.ts +209 -0
  104. package/src/runtime-mcp/pool.ts +224 -23
  105. package/src/runtime-mcp/transports/stdio.ts +158 -24
  106. package/src/runtime-mcp/types.ts +24 -0
  107. package/src/sdk/acp/adapter.ts +1 -1
  108. package/src/sdk/broker/broker.ts +5 -4
  109. package/src/sdk/bus/chat-daemon-control.ts +1 -1
  110. package/src/sdk/bus/chat-daemon-runtime.ts +44 -1
  111. package/src/sdk/bus/slack-daemon.ts +92 -2
  112. package/src/sdk/bus/slack-live-provider.ts +12 -0
  113. package/src/sdk/bus/slack-provider.ts +5 -0
  114. package/src/sdk/cli/rows.ts +47 -2
  115. package/src/sdk/cli/session-cli.ts +47 -23
  116. package/src/sdk/host/events.ts +25 -2
  117. package/src/sdk/host/host.ts +4 -1
  118. package/src/sdk/host/session-runtime.test.ts +36 -25
  119. package/src/sdk/host/session-runtime.ts +279 -43
  120. package/src/sdk/protocol/operation-inventory.generated.json +66 -0
  121. package/src/sdk/router/session-router.ts +60 -37
  122. package/src/sdk/session-api-key.ts +94 -0
  123. package/src/sdk/session.ts +73 -30
  124. package/src/sdk/transport/serve-cli.ts +3 -3
  125. package/src/session/agent-session.ts +123 -38
  126. package/src/session/contribution-prep.ts +214 -21
  127. package/src/setup/paseo/announce.ts +450 -0
  128. package/src/setup/paseo/setup-deps.ts +15 -3
  129. package/src/slash-commands/builtin-registry.ts +1 -0
  130. package/src/system-prompt.ts +4 -0
  131. package/src/task/executor.ts +2 -1
  132. package/src/task/index.ts +17 -4
  133. package/src/tools/bash.ts +50 -21
  134. package/src/tools/cron.ts +1 -1
  135. package/src/tools/fetch.ts +29 -1
  136. package/src/tools/index.ts +8 -2
  137. package/src/tools/job.ts +8 -2
  138. package/src/tools/monitor.ts +8 -2
  139. package/src/tools/path-utils.ts +7 -0
  140. package/src/tools/search.ts +116 -58
  141. package/src/tools/skill.ts +22 -2
  142. package/src/tools/subagent.ts +3 -1
  143. package/src/tools/tool-catalog.generated.ts +9 -2
  144. package/src/utils/external-editor.ts +16 -2
  145. package/src/web/insane/bridge.ts +113 -23
  146. package/src/web/search/providers/perplexity.ts +102 -41
@@ -128,6 +128,15 @@ type AgentSessionEventHandlers = {
128
128
  [E in AgentSessionEventKind]: (event: Extract<AgentSessionEvent, { type: E }>) => Promise<void>;
129
129
  };
130
130
 
131
+ interface AssistantTextPresentation {
132
+ component: AssistantMessageComponent;
133
+ latestMessage: AssistantMessage;
134
+ sessionIdentity: InteractiveModeContext["session"];
135
+ sessionEpoch: number;
136
+ paintEpoch: number;
137
+ cancel: () => void;
138
+ }
139
+
131
140
  export class EventController {
132
141
  #lastReadGroup: ReadToolGroupComponent | undefined = undefined;
133
142
  #lastThinkingCount = 0;
@@ -146,6 +155,16 @@ export class EventController {
146
155
  #visibleTranscriptChanged = false;
147
156
  #handlingEvent = false;
148
157
  #eventQueue: Promise<void> = Promise.resolve();
158
+ #disposed = false;
159
+ #assistantTextSuspended = false;
160
+ #sessionPresentationEpoch = 0;
161
+ #paintEpoch = 0;
162
+ #pendingAssistantText: AssistantTextPresentation | undefined;
163
+ #assistantTextMutationScope: AssistantTextPresentation | undefined;
164
+ #assistantLifetimes = new WeakMap<
165
+ AssistantMessageComponent,
166
+ { sessionIdentity: InteractiveModeContext["session"]; sessionEpoch: number }
167
+ >();
149
168
 
150
169
  constructor(private ctx: InteractiveModeContext) {
151
170
  this.#handlers = {
@@ -177,6 +196,8 @@ export class EventController {
177
196
  }
178
197
 
179
198
  dispose(): void {
199
+ this.#disposed = true;
200
+ this.resetAssistantTextPresentation();
180
201
  this.#cancelIdleCompaction();
181
202
  this.#clearRetryCountdown();
182
203
  if (this.ctx.retryEscapeHandler) {
@@ -199,6 +220,104 @@ export class EventController {
199
220
  this.clearIrcExpiryTimers();
200
221
  }
201
222
 
223
+ #cancelAssistantTextPresentation(): void {
224
+ const pending = this.#pendingAssistantText;
225
+ this.#pendingAssistantText = undefined;
226
+ pending?.cancel();
227
+ }
228
+
229
+ /** Temporary TUI stop discards scheduling, not historical assistant callback lifetimes. */
230
+ suspendAssistantTextPresentation(): void {
231
+ this.#assistantTextSuspended = true;
232
+ this.#paintEpoch += 1;
233
+ this.#cancelAssistantTextPresentation();
234
+ }
235
+
236
+ /** Rearm before the first restarted frame, even without a new provider event. */
237
+ resumeAssistantTextPresentation(): void {
238
+ this.#cancelAssistantTextPresentation();
239
+ this.#assistantTextSuspended = false;
240
+ const component = this.ctx.streamingComponent;
241
+ const message = this.ctx.streamingMessage;
242
+ if (component && message && this.#isLiveAssistant(component)) {
243
+ this.#queueAssistantText(component, message);
244
+ }
245
+ }
246
+
247
+ /** Session/transcript replacement invalidates callbacks before removing old children. */
248
+ resetAssistantTextPresentation(): void {
249
+ this.#sessionPresentationEpoch += 1;
250
+ this.#cancelAssistantTextPresentation();
251
+ }
252
+
253
+ #isLiveAssistant(component: AssistantMessageComponent): boolean {
254
+ const lifetime = this.#assistantLifetimes.get(component);
255
+ return (
256
+ !this.#disposed &&
257
+ !this.ctx.isStopped?.() &&
258
+ lifetime !== undefined &&
259
+ lifetime.sessionIdentity === this.ctx.session &&
260
+ lifetime.sessionEpoch === this.#sessionPresentationEpoch &&
261
+ this.ctx.chatContainer.hasLiveChild(component)
262
+ );
263
+ }
264
+
265
+ #isValidAssistantText(record: AssistantTextPresentation): boolean {
266
+ return (
267
+ !this.#assistantTextSuspended &&
268
+ this.#isLiveAssistant(record.component) &&
269
+ record.sessionIdentity === this.ctx.session &&
270
+ record.sessionEpoch === this.#sessionPresentationEpoch &&
271
+ record.paintEpoch === this.#paintEpoch &&
272
+ record.component === this.ctx.streamingComponent
273
+ );
274
+ }
275
+
276
+ #queueAssistantText(component: AssistantMessageComponent, latestMessage: AssistantMessage): void {
277
+ if (this.#assistantTextSuspended || !this.#isLiveAssistant(component)) return;
278
+ const pending = this.#pendingAssistantText;
279
+ if (pending && this.#isValidAssistantText(pending)) {
280
+ pending.latestMessage = latestMessage;
281
+ return;
282
+ }
283
+ this.#cancelAssistantTextPresentation();
284
+ const record: AssistantTextPresentation = {
285
+ component,
286
+ latestMessage,
287
+ sessionIdentity: this.ctx.session,
288
+ sessionEpoch: this.#sessionPresentationEpoch,
289
+ paintEpoch: this.#paintEpoch,
290
+ cancel: () => {},
291
+ };
292
+ this.#pendingAssistantText = record;
293
+ record.cancel = this.ctx.ui.enqueueBeforeRender(() => this.#flushAssistantText(record));
294
+ }
295
+
296
+ #flushAssistantText(record = this.#pendingAssistantText): void {
297
+ if (!record || this.#pendingAssistantText !== record) return;
298
+ this.#cancelAssistantTextPresentation();
299
+ if (!this.#isValidAssistantText(record)) return;
300
+ const previousScope = this.#assistantTextMutationScope;
301
+ this.#assistantTextMutationScope = record;
302
+ try {
303
+ record.component.updateContent(record.latestMessage, { streaming: true });
304
+ } finally {
305
+ this.#assistantTextMutationScope = previousScope;
306
+ }
307
+ }
308
+
309
+ #observeAssistantMutation(component: AssistantMessageComponent): void {
310
+ if (!this.#isLiveAssistant(component)) return;
311
+ const scope = this.#assistantTextMutationScope;
312
+ if (scope?.component === component && this.#isValidAssistantText(scope)) {
313
+ // Only this synchronous projection publishes before frame source selection.
314
+ // An unrelated awaited event retains its own dirty flag and settlement semantics.
315
+ this.ctx.recordVisibleTranscriptMutation?.();
316
+ return;
317
+ }
318
+ this.#observeVisibleTranscriptMutation();
319
+ }
320
+
202
321
  #recordVisibleTranscriptMutation(): void {
203
322
  this.#visibleTranscriptChanged = true;
204
323
  }
@@ -292,9 +411,10 @@ export class EventController {
292
411
  }
293
412
 
294
413
  async #dispatchEvent(event: AgentSessionEvent): Promise<void> {
414
+ if (this.#disposed) return;
295
415
  if (this.ctx.isStopped?.()) return;
296
416
  if (!this.ctx.isInitialized) await this.ctx.init();
297
- if (this.ctx.isStopped?.()) return;
417
+ if (this.#disposed || this.ctx.isStopped?.()) return;
298
418
  this.#visibleTranscriptChanged = false;
299
419
  this.#handlingEvent = true;
300
420
  try {
@@ -462,18 +582,26 @@ export class EventController {
462
582
  this.ctx.ui.requestRender();
463
583
  this.#recordVisibleTranscriptMutation();
464
584
  } else if (event.message.role === "assistant") {
585
+ this.#cancelAssistantTextPresentation();
465
586
  this.#lastThinkingCount = 0;
466
587
  this.#resetReadGroup();
467
588
  this.#toolIntentCache.clear();
468
589
  this.#thinkingContentIndices.clear();
469
- this.ctx.streamingComponent = new AssistantMessageComponent(
590
+ const component: AssistantMessageComponent = new AssistantMessageComponent(
470
591
  undefined,
471
592
  this.ctx.hideThinkingBlock,
472
- () => this.ctx.ui.requestRender(),
593
+ () => {
594
+ if (this.#isLiveAssistant(component)) this.ctx.ui.requestRender();
595
+ },
473
596
  this.ctx.getAssistantViewportAnchorId?.(event.message),
474
- () => this.#observeVisibleTranscriptMutation(),
597
+ () => this.#observeAssistantMutation(component),
475
598
  this.ctx.session,
476
599
  );
600
+ this.#assistantLifetimes.set(component, {
601
+ sessionIdentity: this.ctx.session,
602
+ sessionEpoch: this.#sessionPresentationEpoch,
603
+ });
604
+ this.ctx.streamingComponent = component;
477
605
  this.ctx.streamingMessage = event.message;
478
606
  addChatChild(this.ctx, this.ctx.streamingComponent);
479
607
  this.ctx.streamingComponent.updateContent(this.ctx.streamingMessage, { streaming: true });
@@ -605,8 +733,20 @@ export class EventController {
605
733
  transferSessionMessageIdentity([this.ctx.streamingMessage], [event.message]);
606
734
  }
607
735
  this.ctx.streamingMessage = event.message;
608
- this.ctx.streamingComponent.updateContent(this.ctx.streamingMessage, { streaming: true });
609
736
  const contentIndex = event.assistantMessageEvent?.contentIndex;
737
+ if (
738
+ event.assistantMessageEvent?.type === "text_delta" &&
739
+ typeof contentIndex === "number" &&
740
+ Number.isInteger(contentIndex) &&
741
+ contentIndex >= 0 &&
742
+ contentIndex < event.message.content.length &&
743
+ event.message.content[contentIndex]?.type === "text"
744
+ ) {
745
+ this.#queueAssistantText(this.ctx.streamingComponent, event.message);
746
+ } else {
747
+ this.#cancelAssistantTextPresentation();
748
+ this.ctx.streamingComponent.updateContent(this.ctx.streamingMessage, { streaming: true });
749
+ }
610
750
  const changedContent =
611
751
  typeof contentIndex === "number" && contentIndex >= 0
612
752
  ? this.ctx.streamingMessage.content[contentIndex]
@@ -714,6 +854,7 @@ export class EventController {
714
854
 
715
855
  async #handleMessageEnd(event: Extract<AgentSessionEvent, { type: "message_end" }>): Promise<void> {
716
856
  if (event.message.role === "user") return;
857
+ if (event.message.role === "assistant") this.#cancelAssistantTextPresentation();
717
858
  if (this.ctx.streamingComponent && event.message.role === "assistant") {
718
859
  if (this.ctx.streamingMessage?.role === "assistant") {
719
860
  transferSessionMessageIdentity([this.ctx.streamingMessage], [event.message]);
@@ -916,6 +1057,8 @@ export class EventController {
916
1057
  }
917
1058
 
918
1059
  async #handleAgentEnd(event: Extract<AgentSessionEvent, { type: "agent_end" }>): Promise<void> {
1060
+ this.#flushAssistantText();
1061
+ this.#cancelAssistantTextPresentation();
919
1062
  this.ctx.setWorkingMessage(undefined);
920
1063
  stopInteractiveActivityIndicator(this.ctx, { foregroundSettled: true });
921
1064
  if (this.ctx.streamingComponent) {
@@ -1976,6 +1976,22 @@ export class SelectorController {
1976
1976
  width ?? this.ctx.editor.getTopBorderAvailableWidth(this.ctx.ui.terminal.columns);
1977
1977
  return this.ctx.statusLine.getPreviewContent(availableWidth);
1978
1978
  },
1979
+ getStatusLinePreviewForSettings: (previewSettings, width?: number) => {
1980
+ const availableWidth =
1981
+ width ?? this.ctx.editor.getTopBorderAvailableWidth(this.ctx.ui.terminal.columns);
1982
+ return this.ctx.statusLine.getPreviewContentForSettings(availableWidth, {
1983
+ ...buildStatusLineSettings(settings),
1984
+ ...previewSettings,
1985
+ });
1986
+ },
1987
+ getStatusLinePreviewPartsForSettings: (previewSettings, width?: number) => {
1988
+ const availableWidth =
1989
+ width ?? this.ctx.editor.getTopBorderAvailableWidth(this.ctx.ui.terminal.columns);
1990
+ return this.ctx.statusLine.getPreviewPartsForSettings(availableWidth, {
1991
+ ...buildStatusLineSettings(settings),
1992
+ ...previewSettings,
1993
+ });
1994
+ },
1979
1995
  onPluginsChanged: () => {
1980
1996
  this.ctx.ui.requestRender();
1981
1997
  },
@@ -2982,6 +2998,8 @@ export class SelectorController {
2982
2998
  this.ctx.ui.requestRender();
2983
2999
  try {
2984
3000
  await mgr.uninstallPlugin(pluginId, scope);
3001
+ await this.ctx.session.reloadSkills();
3002
+ await this.ctx.refreshSlashCommandState();
2985
3003
  this.ctx.showStatus(`Uninstalled ${pluginId}`);
2986
3004
  } catch (err) {
2987
3005
  this.ctx.showStatus(`Uninstall failed: ${err}`);
@@ -3019,6 +3037,8 @@ export class SelectorController {
3019
3037
  try {
3020
3038
  const force = installedIds.has(`${name}@${marketplace}`);
3021
3039
  await mgr.installPlugin(name, marketplace, { force });
3040
+ await this.ctx.session.reloadSkills();
3041
+ await this.ctx.refreshSlashCommandState();
3022
3042
  this.ctx.showStatus(`Installed ${name} from ${marketplace}`);
3023
3043
  } catch (err) {
3024
3044
  this.ctx.showStatus(`Install failed: ${err}`);
@@ -3245,6 +3265,7 @@ export class SelectorController {
3245
3265
  }
3246
3266
 
3247
3267
  #clearTransientSessionUi(options?: { restoreBackground?: boolean; clearSpecializedLoaders?: boolean }): void {
3268
+ this.ctx.resetAssistantTextPresentation();
3248
3269
  if (options?.clearSpecializedLoaders) clearInteractiveActivityLoaders(this.ctx);
3249
3270
  stopInteractiveActivityIndicator(this.ctx, {
3250
3271
  restoreBackground: options?.restoreBackground,
@@ -13,7 +13,7 @@ import {
13
13
  Text,
14
14
  TUI,
15
15
  } from "@gajae-code/tui";
16
- import { APP_NAME, adjustHsv, getProjectDir, logger, postmortem, sanitizeText } from "@gajae-code/utils";
16
+ import { APP_NAME, adjustHsv, getAgentDir, getProjectDir, logger, postmortem, sanitizeText } from "@gajae-code/utils";
17
17
  import chalk from "chalk";
18
18
  import { AsyncJobManager } from "../async";
19
19
  import {
@@ -49,6 +49,7 @@ import type { AgentSession, AgentSessionEvent, AsyncJobSnapshotItem } from "../s
49
49
  import type { HistoryStorage } from "../session/history-storage";
50
50
  import type { SessionContext, SessionManager } from "../session/session-manager";
51
51
  import { getRecentSessions, getSessionMessageEntryId } from "../session/session-manager";
52
+ import { createDefaultPaseoAnnounceDependencies, startPaseoAnnouncement } from "../setup/paseo/announce";
52
53
  import type { LspStartupServerInfo } from "../tools";
53
54
  import { formatPhaseDisplayName } from "../tools/todo-write";
54
55
  import { copyToClipboard } from "../utils/clipboard";
@@ -745,6 +746,10 @@ export class InteractiveMode implements InteractiveModeContext {
745
746
  version: this.#version,
746
747
  focusDomain: "composer",
747
748
  keyDisplayContext: this.#keyDisplayContext,
749
+ onUpdate: () => {
750
+ this.updateEditorChrome();
751
+ this.ui.requestRender();
752
+ },
748
753
  });
749
754
  this.statusLine.setAutoCompactEnabled(session.autoCompactionEnabled);
750
755
 
@@ -776,6 +781,10 @@ export class InteractiveMode implements InteractiveModeContext {
776
781
  this.#btwController = new BtwController(this);
777
782
  this.#extensionUiController = new ExtensionUiController(this);
778
783
  this.#eventController = new EventController(this);
784
+ this.ui.setRenderPreparationLifecycleCallbacks({
785
+ invalidate: () => this.#eventController.suspendAssistantTextPresentation(),
786
+ beforeStart: () => this.#eventController.resumeAssistantTextPresentation(),
787
+ });
779
788
  this.#commandController = new CommandController(this);
780
789
  this.#todoCommandController = new TodoCommandController(this);
781
790
  this.#selectorController = new SelectorController(this);
@@ -1065,6 +1074,37 @@ export class InteractiveMode implements InteractiveModeContext {
1065
1074
  crashRelayTimer.unref?.();
1066
1075
  }
1067
1076
 
1077
+ // Make this session visible in Paseo. Interactive-only by construction: a
1078
+ // `gjc acp` provider process never builds an InteractiveMode, so an imported
1079
+ // session can never announce itself back into Paseo.
1080
+ if (this.settings.getGlobal("paseo.autoImport") === true) {
1081
+ const sessionId = this.sessionManager.getSessionId();
1082
+ const announcement = startPaseoAnnouncement(
1083
+ {
1084
+ sessionId,
1085
+ cwd: this.sessionManager.getCwd(),
1086
+ isEnabled: () => this.settings.getGlobal("paseo.autoImport") === true,
1087
+ onOutcome: outcome => {
1088
+ if (this.#stopped || outcome.kind === "skipped") return;
1089
+ this.showStatus(
1090
+ outcome.kind === "failed"
1091
+ ? `Paseo import failed: ${outcome.detail}`
1092
+ : `Controllable in Paseo as ${outcome.providerKey}`,
1093
+ { dim: true },
1094
+ );
1095
+ },
1096
+ },
1097
+ createDefaultPaseoAnnounceDependencies(getAgentDir()),
1098
+ );
1099
+ const stopWatchingSettings = this.settings.onChanged(path => {
1100
+ if (path === "paseo.autoImport" && this.settings.getGlobal(path) !== true) announcement.cancel();
1101
+ });
1102
+ this.#stopListeners.add(() => {
1103
+ stopWatchingSettings();
1104
+ announcement.cancel();
1105
+ });
1106
+ }
1107
+
1068
1108
  if (starReminderGate.schedule) {
1069
1109
  scheduleLaunchStarReminderAfterFirstRender({
1070
1110
  confirm: (title, message) =>
@@ -1517,6 +1557,7 @@ export class InteractiveMode implements InteractiveModeContext {
1517
1557
 
1518
1558
  rebuildChatFromMessages(policy: TranscriptRebuildPolicy): void {
1519
1559
  prepareTranscriptRebuild(this.ui, policy);
1560
+ this.resetAssistantTextPresentation();
1520
1561
  this.chatContainer.clear();
1521
1562
  const context = this.session.buildDisplaySessionContext();
1522
1563
  this.renderSessionContext(context);
@@ -1649,6 +1690,7 @@ export class InteractiveMode implements InteractiveModeContext {
1649
1690
  stop(): void {
1650
1691
  const wasInitialized = this.isInitialized;
1651
1692
  this.#stopped = true;
1693
+ this.ui.setRenderPreparationLifecycleCallbacks(undefined);
1652
1694
  this.#inputController.discardDeferredSubmission();
1653
1695
  for (const listener of this.#stopListeners) {
1654
1696
  try {
@@ -2077,6 +2119,10 @@ export class InteractiveMode implements InteractiveModeContext {
2077
2119
  return this.#uiHelpers.isKnownSlashCommand(text);
2078
2120
  }
2079
2121
 
2122
+ resetAssistantTextPresentation(): void {
2123
+ this.#eventController.resetAssistantTextPresentation();
2124
+ }
2125
+
2080
2126
  /** Advances the sticky-viewport source only for semantic transcript output. */
2081
2127
  recordVisibleTranscriptMutation(): void {
2082
2128
  const identity = `session:${this.sessionManager.getSessionId()}`;
@@ -355,6 +355,7 @@ export interface InteractiveModeContext {
355
355
  extractAssistantText(message: AssistantMessage): string;
356
356
  /** Records one semantic visible-transcript mutation for the sticky viewport. */
357
357
  recordVisibleTranscriptMutation?(): void;
358
+ resetAssistantTextPresentation(): void;
358
359
  updateEditorTopBorder(): void;
359
360
  updateEditorBorderColor(): void;
360
361
  rebuildChatFromMessages(policy: TranscriptRebuildPolicy): void;
@@ -944,6 +944,7 @@ export class UiHelpers {
944
944
  this.ctx.pendingPythonComponents = this.ctx.pendingPythonComponents.filter(component =>
945
945
  runningExecutionComponents.includes(component),
946
946
  );
947
+ this.ctx.resetAssistantTextPresentation();
947
948
  this.ctx.chatContainer.clear();
948
949
 
949
950
  // Reuse a pre-built context when available (e.g. from navigateTree) to avoid a second O(N) walk.
@@ -47,6 +47,14 @@ Optimize for correctness first, maintainability second, and brevity third. Prefe
47
47
  - Do not defer actionable work. Underpromise and overdeliver: report only what is done or in progress, never announce remaining work instead of doing it.
48
48
  </communication>
49
49
 
50
+ {{#if reasoningLanguageEnglish}}
51
+ <reasoning-language>
52
+ - Reason through development and technical problem-solving in English.
53
+ - Keep user-facing answers in the language the user requested or used.
54
+ - This changes reasoning language only; it does not relax correctness, safety, or communication requirements.
55
+ </reasoning-language>
56
+ {{/if}}
57
+
50
58
  <completion-contract>
51
59
  - Never present partial work as complete.
52
60
  - Never suppress tests or warnings to make code pass.
@@ -18,7 +18,7 @@ Executes bash command in shell session for terminal operations like git, bun, ca
18
18
  {{/if}}
19
19
  {{#if foregroundFoldEnabled}}
20
20
  - In the interactive TUI, the user can press the fold chord twice (default `Alt+Shift+B`, plus `Cmd+B` on macOS; remappable via the `app.tool.backgroundFold` keybinding) while a foreground command is still running to fold it into a background job. All three command surfaces fold: managed non-PTY bash, editor/ACP client-terminal commands, and PTY-mode commands. A folded command keeps its original timeout and is never killed or restarted by folding; a folded PTY continues output-only, so it stops accepting input. The turn then ends cleanly and a later turn resumes automatically with the command's result, so the interrupted task is finished rather than abandoned. `task`/`subagent` waits are deliberately NOT foldable. Do not instruct users to use raw shell `Ctrl+Z`/`bg` inside the GJC TUI; ownership and output routing are not safe there.
21
- - A user steer folds the same way: when the user sends a message while a foreground command has already run for at least 2 seconds, the command is folded into a background job and this call returns the background-start result plus the line `Folded into background job <id> because a user steer arrived; the command keeps running with its original timeout and its result will wake a later turn.` The steer is then delivered at this tool boundary, so handle it next; do not re-run the folded command. Steers that arrive within the first 2 seconds let the command finish normally. This applies only while `busyPromptMode` is `steer` and `toolInterruptPolicy` is `abort_tools`.
21
+ - A user steer folds the same way: when the user sends a message while a foreground command has already run for at least 2 seconds, the command is folded into a background job and this call returns the background-start result plus the line `Folded into background job <id> because a user steer arrived; the command keeps running with its original timeout and its result will wake a later turn.` The steer is then delivered at this tool boundary, so handle it next; do not re-run the folded command. Steers that arrive within the first 2 seconds let the command finish normally. This applies only while `busyPromptMode` is `steer`; `toolInterruptPolicy` is not a gate, since a fold never kills the command.
22
22
  {{/if}}
23
23
  </instruction>
24
24
  {{#if restrictedAllowedPrefixes}}
@@ -2,7 +2,7 @@ Inspects, waits, or cancels async jobs.
2
2
 
3
3
  Background job results are delivered automatically when complete. Jobs that back task subagents should be controlled via the `subagent` tool when it is available; use `job` for non-subagent jobs (async bash, monitors) and as a compatibility fallback. Running job output stays quiet by default to avoid flooding the conversation; use `tail` when you explicitly want to show/reopen retained output. Reach for this tool only when you need to inspect or intervene.
4
4
 
5
- In the interactive TUI, a running foreground command can be folded into a background job by pressing the fold chord twice (default `Alt+Shift+B`, plus `Cmd+B` on macOS; remappable via `app.tool.backgroundFold`), or automatically when the user sends a steer after the command has run for at least 2 seconds (with `busyPromptMode=steer` and `toolInterruptPolicy=abort_tools`). This covers managed non-PTY bash, editor/ACP client-terminal commands, and PTY-mode commands. A folded job keeps its original deadline, is never killed or restarted by folding, and its result wakes a later turn automatically; a folded PTY continues output-only and no longer accepts input. Job listings expose `foldReason` (`steer`, `chord`, `timer`, or `sdk_control`) beside `backgrounded` so you can tell why a job left the foreground. `task`/`subagent` waits are not foldable. Raw shell `Ctrl+Z`/`bg` is not the supported path inside GJC because it bypasses job ownership and output-routing contracts.
5
+ In the interactive TUI, a running foreground command can be folded into a background job by pressing the fold chord twice (default `Alt+Shift+B`, plus `Cmd+B` on macOS; remappable via `app.tool.backgroundFold`), or automatically when the user sends a steer after the command has run for at least 2 seconds (with `busyPromptMode=steer` (`toolInterruptPolicy` is not a gate)). This covers managed non-PTY bash, editor/ACP client-terminal commands, and PTY-mode commands. A folded job keeps its original deadline, is never killed or restarted by folding, and its result wakes a later turn automatically; a folded PTY continues output-only and no longer accepts input. Job listings expose `foldReason` (`steer`, `chord`, `timer`, or `sdk_control`) beside `backgrounded` so you can tell why a job left the foreground. `task`/`subagent` waits are not foldable. Raw shell `Ctrl+Z`/`bg` is not the supported path inside GJC because it bypasses job ownership and output-routing contracts.
6
6
 
7
7
  # Operations
8
8
 
@@ -5,6 +5,7 @@ Searches files using powerful regex matching.
5
5
  - `paths` accepts an array of files, directories, globs, or internal URLs; when omitted, the whole working directory is searched
6
6
  - `paths` is an array; do not embed commas or spaces inside a single entry. Pass `["src", "tests"]` not `["src,tests"]`.
7
7
  - Cross-line patterns are detected from literal `\n` or escaped `\\n` in `pattern`
8
+ - `timeout` is in seconds (default 5, range 0.5–60). Increase it only for intentionally broad searches; narrow `paths` first
8
9
  </instruction>
9
10
 
10
11
  <output>
@@ -20,4 +21,5 @@ Searches files using powerful regex matching.
20
21
  <critical>
21
22
  - Search paths are an array; pass separate entries rather than comma-joined paths.
22
23
  - Use a cross-line pattern only when the match actually spans lines.
24
+ - Do not retry a timed-out search unchanged; narrow `paths`/`pattern` or explicitly increase `timeout`.
23
25
  </critical>
@@ -24,8 +24,8 @@ import {
24
24
  normalizeMcpCacheHints,
25
25
  resolveMCPProtocolPreference,
26
26
  } from "./protocol";
27
- import { createHttpTransport, HttpTransport } from "./transports/http";
28
- import { createStdioTransport } from "./transports/stdio";
27
+ import { HttpTransport } from "./transports/http";
28
+ import { StdioTransport } from "./transports/stdio";
29
29
  import type {
30
30
  MCPDiscoverResultLite,
31
31
  MCPGetPromptParams,
@@ -188,18 +188,18 @@ async function defaultRequestHandler(method: string, _params: unknown): Promise<
188
188
  /**
189
189
  * Create a transport for the given server config.
190
190
  */
191
- async function createTransport(config: MCPServerConfig): Promise<MCPTransport> {
191
+ function createTransport(config: MCPServerConfig): StdioTransport | HttpTransport {
192
192
  const serverType = config.type ?? "stdio";
193
193
 
194
194
  switch (serverType) {
195
195
  case "stdio":
196
- return createStdioTransport(config as MCPStdioServerConfig);
196
+ return new StdioTransport(config as MCPStdioServerConfig);
197
197
  case "http":
198
- return createHttpTransport(config as MCPHttpServerConfig);
198
+ return new HttpTransport(config as MCPHttpServerConfig);
199
199
  case "sse":
200
200
  // Compatibility: `sse` configs use Streamable HTTP, not the legacy MCP SSE
201
201
  // protocol. The configured URL receives JSON-RPC POST requests directly.
202
- return createHttpTransport(config as MCPSseServerConfig);
202
+ return new HttpTransport(config as MCPSseServerConfig);
203
203
  default:
204
204
  throw new Error(`Unknown server type: ${serverType}`);
205
205
  }
@@ -295,6 +295,42 @@ function unwrapTransportFailure(error: unknown): unknown {
295
295
  return error instanceof MCPExpectedFailure && error.cause !== undefined ? error.cause : error;
296
296
  }
297
297
 
298
+ function transportFailureCauses(error: unknown): unknown[] {
299
+ const failure = unwrapTransportFailure(error);
300
+ return failure instanceof AggregateError ? failure.errors : [failure];
301
+ }
302
+
303
+ function connectionAndCleanupCause(connectionError: unknown, cleanupError: unknown): AggregateError {
304
+ return new AggregateError(
305
+ [...transportFailureCauses(connectionError), ...transportFailureCauses(cleanupError)],
306
+ "MCP connection failed and transport cleanup also failed",
307
+ );
308
+ }
309
+
310
+ function combinedConnectionAndCleanupFailure(connectionError: unknown, cleanupError: unknown): MCPExpectedFailure {
311
+ return new MCPExpectedFailure(connectionAndCleanupCause(connectionError, cleanupError));
312
+ }
313
+
314
+ /** Failed connection whose still-owned transport cleanup can be retried explicitly. */
315
+ export class MCPConnectionCleanupFailure extends MCPExpectedFailure {
316
+ #transport: MCPTransport;
317
+
318
+ constructor(cause: unknown, transport: MCPTransport) {
319
+ super(cause);
320
+ this.name = "MCPConnectionCleanupFailure";
321
+ this.#transport = transport;
322
+ }
323
+
324
+ /** Retry the same retained transport cleanup; failures preserve all prior causes. */
325
+ async close(): Promise<void> {
326
+ try {
327
+ await this.#transport.close();
328
+ } catch (cleanupError) {
329
+ throw new MCPConnectionCleanupFailure(connectionAndCleanupCause(this, cleanupError), this.#transport);
330
+ }
331
+ }
332
+ }
333
+
298
334
  type ModernNegotiation =
299
335
  | { outcome: "modern"; discovery: MCPDiscoverResultLite | null; discoverState: "yes" | "no"; versionRetry: boolean }
300
336
  | { outcome: "legacy-fallback"; reason: MCPDowngradeReason };
@@ -505,14 +541,14 @@ export async function connectToServer(
505
541
  },
506
542
  ): Promise<MCPServerConnection> {
507
543
  const timeoutMs = config.timeout ?? CONNECTION_TIMEOUT_MS;
508
- let transport: MCPTransport | undefined;
544
+ let transport: StdioTransport | HttpTransport | undefined;
509
545
  const connectAbort = new AbortController();
510
546
  const connectSignal = options?.signal ? AbortSignal.any([options.signal, connectAbort.signal]) : connectAbort.signal;
511
547
 
512
548
  const preference = resolveMCPProtocolPreference(config.protocol);
513
549
 
514
550
  const connect = async (): Promise<MCPServerConnection> => {
515
- transport = await createTransport(config);
551
+ transport = createTransport(config);
516
552
  if (options?.onNotification) {
517
553
  transport.onNotification = options.onNotification;
518
554
  }
@@ -561,6 +597,7 @@ export async function connectToServer(
561
597
  };
562
598
 
563
599
  try {
600
+ await transport.connect();
564
601
  // stdio has no per-request HTTP era signals; it stays on the legacy handshake.
565
602
  if ((config.type ?? "stdio") === "stdio") {
566
603
  return await connectLegacy("legacy-forced", "stdio-transport");
@@ -609,8 +646,8 @@ export async function connectToServer(
609
646
  } catch (error) {
610
647
  try {
611
648
  await transport.close();
612
- } catch {
613
- // Preserve the initialization failure when cleanup also fails.
649
+ } catch (cleanupError) {
650
+ throw combinedConnectionAndCleanupFailure(error, cleanupError);
614
651
  }
615
652
  throw error;
616
653
  }
@@ -625,20 +662,26 @@ export async function connectToServer(
625
662
  // abort initialization and wait for transport cleanup before returning.
626
663
  const aborted = options?.signal?.aborted === true;
627
664
  connectAbort.abort(error);
665
+ let failure = error;
666
+ let cleanupIncomplete = false;
628
667
  if (transport) {
629
668
  try {
630
669
  await transport.close();
631
- } catch {
632
- // Preserve the primary connection failure when cleanup also fails.
670
+ } catch (cleanupError) {
671
+ failure = combinedConnectionAndCleanupFailure(failure, cleanupError);
672
+ cleanupIncomplete = true;
633
673
  }
634
674
  }
635
- if (error instanceof MCPExpectedFailure) {
636
- throw error;
675
+ if (cleanupIncomplete && transport) {
676
+ throw new MCPConnectionCleanupFailure(unwrapTransportFailure(failure), transport);
637
677
  }
638
- if (aborted || (error instanceof Error && error.message === connectionTimeoutMessage)) {
639
- throw new MCPExpectedFailure(error);
678
+ if (failure instanceof MCPExpectedFailure) {
679
+ throw failure;
640
680
  }
641
- throw error;
681
+ if (aborted || (failure instanceof Error && failure.message === connectionTimeoutMessage)) {
682
+ throw new MCPExpectedFailure(failure);
683
+ }
684
+ throw failure;
642
685
  }
643
686
  }
644
687