@gajae-code/coding-agent 0.8.2 → 0.9.1

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 (214) hide show
  1. package/CHANGELOG.md +53 -0
  2. package/dist/types/append-only-mode.d.ts +31 -0
  3. package/dist/types/capability/rule.d.ts +8 -0
  4. package/dist/types/cli/fixture-report.d.ts +20 -0
  5. package/dist/types/cli/fixture-report.test.d.ts +1 -0
  6. package/dist/types/cli/nofile-limit.d.ts +11 -0
  7. package/dist/types/cli/update-cli.d.ts +22 -0
  8. package/dist/types/config/settings-schema.d.ts +119 -1
  9. package/dist/types/discovery/index.d.ts +1 -0
  10. package/dist/types/export/html/template.generated.d.ts +1 -1
  11. package/dist/types/export/ttsr.d.ts +14 -3
  12. package/dist/types/gjc-runtime/deep-interview-state.d.ts +5 -2
  13. package/dist/types/gjc-runtime/session-state-sidecar.d.ts +7 -0
  14. package/dist/types/gjc-runtime/state-writer.d.ts +3 -0
  15. package/dist/types/gjc-runtime/team-runtime.d.ts +26 -0
  16. package/dist/types/gjc-runtime/tmux-common.d.ts +1 -0
  17. package/dist/types/gjc-runtime/ultragoal-receipt-freshness.d.ts +42 -0
  18. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +119 -13
  19. package/dist/types/goals/runtime.d.ts +1 -0
  20. package/dist/types/hooks/native-skill-hook.d.ts +12 -0
  21. package/dist/types/modes/components/assistant-message.d.ts +5 -1
  22. package/dist/types/modes/components/status-line.d.ts +9 -0
  23. package/dist/types/modes/components/thinking-selector.d.ts +2 -2
  24. package/dist/types/modes/controllers/command-controller.d.ts +1 -0
  25. package/dist/types/modes/controllers/event-controller.d.ts +5 -0
  26. package/dist/types/modes/controllers/selector-controller.d.ts +1 -0
  27. package/dist/types/modes/interactive-mode.d.ts +4 -0
  28. package/dist/types/modes/rpc/rpc-mode.d.ts +1 -3
  29. package/dist/types/modes/rpc/rpc-socket-security.d.ts +3 -0
  30. package/dist/types/modes/rpc/rpc-types.d.ts +14 -0
  31. package/dist/types/modes/shared/agent-wire/event-contract.d.ts +19 -0
  32. package/dist/types/modes/shared/agent-wire/event-envelope.d.ts +23 -1
  33. package/dist/types/modes/types.d.ts +3 -0
  34. package/dist/types/modes/utils/injected-user-submission.d.ts +52 -0
  35. package/dist/types/modes/utils/ui-helpers.d.ts +1 -0
  36. package/dist/types/notifications/config-commands.d.ts +9 -0
  37. package/dist/types/notifications/config.d.ts +22 -0
  38. package/dist/types/notifications/index.d.ts +2 -0
  39. package/dist/types/notifications/lifecycle-commands.d.ts +1 -0
  40. package/dist/types/notifications/lifecycle-control-runtime.d.ts +2 -1
  41. package/dist/types/notifications/reply-sent-store.d.ts +53 -0
  42. package/dist/types/notifications/rich-draft.d.ts +68 -0
  43. package/dist/types/notifications/rich-render.d.ts +90 -0
  44. package/dist/types/notifications/telegram-daemon-cli.d.ts +3 -3
  45. package/dist/types/notifications/telegram-daemon.d.ts +30 -0
  46. package/dist/types/notifications/telegram-reference.d.ts +7 -0
  47. package/dist/types/notifications/threaded-render.d.ts +14 -0
  48. package/dist/types/sdk.d.ts +3 -0
  49. package/dist/types/session/agent-session.d.ts +43 -1
  50. package/dist/types/session/file-mention-pruning.d.ts +8 -0
  51. package/dist/types/session/messages.d.ts +4 -0
  52. package/dist/types/session/session-manager.d.ts +17 -1
  53. package/dist/types/skill-state/active-state.d.ts +18 -1
  54. package/dist/types/system-prompt.d.ts +18 -2
  55. package/dist/types/task/executor.d.ts +1 -0
  56. package/dist/types/task/fork-context-budget.d.ts +7 -0
  57. package/dist/types/task/token-log.d.ts +30 -0
  58. package/dist/types/task/token-log.test.d.ts +1 -0
  59. package/dist/types/tools/bisect.d.ts +81 -0
  60. package/dist/types/tools/index.d.ts +1 -0
  61. package/dist/types/tools/search.d.ts +2 -2
  62. package/dist/types/utils/file-mentions.d.ts +14 -0
  63. package/dist/types/utils/git.d.ts +32 -0
  64. package/dist/types/utils/pasted-image-path.d.ts +30 -0
  65. package/dist/types/web/scrapers/youtube.d.ts +8 -0
  66. package/package.json +8 -11
  67. package/src/append-only-mode.ts +58 -0
  68. package/src/autoresearch/prompt.md +1 -1
  69. package/src/autoresearch/tools/log-experiment.ts +1 -1
  70. package/src/capability/rule.ts +9 -0
  71. package/src/cli/config-cli.ts +27 -3
  72. package/src/cli/fixture-report.test.ts +230 -0
  73. package/src/cli/fixture-report.ts +171 -0
  74. package/src/cli/nofile-limit.ts +48 -0
  75. package/src/cli/notify-cli.ts +11 -9
  76. package/src/cli/update-cli.ts +85 -9
  77. package/src/cli.ts +26 -0
  78. package/src/commands/contribution-prep.ts +1 -1
  79. package/src/config/keybindings.ts +1 -1
  80. package/src/config/settings-schema.ts +76 -1
  81. package/src/defaults/gjc/skills/deep-interview/SKILL.md +12 -13
  82. package/src/defaults/gjc/skills/ralplan/SKILL.md +30 -93
  83. package/src/defaults/gjc/skills/team/SKILL.md +16 -13
  84. package/src/defaults/gjc/skills/ultragoal/SKILL.md +59 -4
  85. package/src/defaults/gjc/skills/ultragoal/pipeline-validation-contracts.md +30 -0
  86. package/src/defaults/gjc-defaults.ts +9 -0
  87. package/src/discovery/helpers.ts +10 -0
  88. package/src/discovery/index.ts +1 -0
  89. package/src/export/html/template.generated.ts +1 -1
  90. package/src/export/html/template.js +60 -2
  91. package/src/export/ttsr.ts +96 -18
  92. package/src/extensibility/extensions/runner.ts +181 -219
  93. package/src/extensibility/gjc-plugins/runtime-adapters.ts +156 -44
  94. package/src/gjc-runtime/deep-interview-state.ts +31 -2
  95. package/src/gjc-runtime/launch-tmux.ts +6 -0
  96. package/src/gjc-runtime/session-state-sidecar.ts +208 -13
  97. package/src/gjc-runtime/state-writer.ts +15 -0
  98. package/src/gjc-runtime/team-runtime.ts +175 -19
  99. package/src/gjc-runtime/tmux-common.ts +1 -0
  100. package/src/gjc-runtime/ultragoal-guard.ts +55 -66
  101. package/src/gjc-runtime/ultragoal-receipt-freshness.ts +315 -0
  102. package/src/gjc-runtime/ultragoal-runtime.ts +1602 -143
  103. package/src/gjc-runtime/workflow-manifest.generated.json +113 -2
  104. package/src/gjc-runtime/workflow-manifest.ts +74 -1
  105. package/src/goals/runtime.ts +10 -2
  106. package/src/hindsight/backend.ts +1 -3
  107. package/src/hooks/native-skill-hook.ts +95 -4
  108. package/src/internal-urls/docs-index.generated.ts +18 -9
  109. package/src/lsp/client.ts +1 -0
  110. package/src/main.ts +43 -2
  111. package/src/memories/index.ts +12 -1
  112. package/src/modes/components/assistant-message.ts +241 -82
  113. package/src/modes/components/custom-editor.ts +1 -0
  114. package/src/modes/components/footer.ts +7 -2
  115. package/src/modes/components/status-line.ts +167 -54
  116. package/src/modes/components/thinking-selector.ts +10 -8
  117. package/src/modes/components/tool-execution.ts +44 -19
  118. package/src/modes/controllers/command-controller.ts +39 -2
  119. package/src/modes/controllers/event-controller.ts +77 -49
  120. package/src/modes/controllers/extension-ui-controller.ts +8 -1
  121. package/src/modes/controllers/input-controller.ts +13 -18
  122. package/src/modes/controllers/selector-controller.ts +41 -0
  123. package/src/modes/interactive-mode.ts +14 -1
  124. package/src/modes/rpc/rpc-mode.ts +32 -12
  125. package/src/modes/rpc/rpc-socket-security.ts +13 -1
  126. package/src/modes/rpc/rpc-types.ts +9 -0
  127. package/src/modes/shared/agent-wire/command-validation.ts +4 -0
  128. package/src/modes/shared/agent-wire/event-contract.ts +22 -0
  129. package/src/modes/shared/agent-wire/event-envelope.ts +82 -0
  130. package/src/modes/shared/agent-wire/scopes.ts +1 -0
  131. package/src/modes/types.ts +3 -0
  132. package/src/modes/utils/injected-user-submission.ts +94 -0
  133. package/src/modes/utils/ui-helpers.ts +17 -4
  134. package/src/notifications/config-commands.ts +20 -0
  135. package/src/notifications/config.ts +43 -4
  136. package/src/notifications/index.ts +105 -9
  137. package/src/notifications/lifecycle-commands.ts +37 -16
  138. package/src/notifications/lifecycle-control-runtime.ts +8 -3
  139. package/src/notifications/reply-sent-store.ts +134 -0
  140. package/src/notifications/rich-draft.ts +107 -0
  141. package/src/notifications/rich-render.ts +142 -0
  142. package/src/notifications/telegram-daemon-cli.ts +54 -7
  143. package/src/notifications/telegram-daemon-control.ts +2 -2
  144. package/src/notifications/telegram-daemon.ts +390 -56
  145. package/src/notifications/telegram-reference.ts +17 -0
  146. package/src/notifications/threaded-render.ts +45 -2
  147. package/src/prompts/agents/architect.md +19 -10
  148. package/src/prompts/agents/critic.md +16 -18
  149. package/src/prompts/agents/executor.md +14 -11
  150. package/src/prompts/agents/planner.md +18 -13
  151. package/src/prompts/ci-green-request.md +3 -8
  152. package/src/prompts/goals/goal-continuation.md +0 -5
  153. package/src/prompts/goals/goal-mode-active.md +0 -5
  154. package/src/prompts/system/agent-creation-architect.md +2 -2
  155. package/src/prompts/system/plan-mode-active.md +1 -1
  156. package/src/prompts/system/plan-mode-subagent.md +3 -3
  157. package/src/prompts/system/project-prompt.md +0 -13
  158. package/src/prompts/system/subagent-system-prompt.md +3 -2
  159. package/src/prompts/system/system-prompt.md +26 -19
  160. package/src/prompts/system/volatile-project-context.md +10 -0
  161. package/src/prompts/system/web-search.md +4 -7
  162. package/src/prompts/tools/apply-patch.md +6 -0
  163. package/src/prompts/tools/ast-edit.md +1 -0
  164. package/src/prompts/tools/bash.md +1 -1
  165. package/src/prompts/tools/bisect.md +22 -0
  166. package/src/prompts/tools/browser.md +1 -1
  167. package/src/prompts/tools/cron.md +0 -2
  168. package/src/prompts/tools/eval.md +2 -2
  169. package/src/prompts/tools/find.md +1 -1
  170. package/src/prompts/tools/github.md +6 -6
  171. package/src/prompts/tools/goal.md +1 -3
  172. package/src/prompts/tools/image-gen.md +12 -5
  173. package/src/prompts/tools/irc.md +5 -4
  174. package/src/prompts/tools/job.md +2 -2
  175. package/src/prompts/tools/lsp.md +1 -1
  176. package/src/prompts/tools/monitor.md +2 -2
  177. package/src/prompts/tools/patch.md +2 -2
  178. package/src/prompts/tools/replace.md +2 -21
  179. package/src/prompts/tools/rewind.md +1 -1
  180. package/src/prompts/tools/search-tool-bm25.md +6 -11
  181. package/src/prompts/tools/search.md +1 -1
  182. package/src/prompts/tools/skill.md +3 -2
  183. package/src/prompts/tools/ssh.md +2 -1
  184. package/src/prompts/tools/subagent.md +2 -2
  185. package/src/prompts/tools/task.md +4 -5
  186. package/src/prompts/tools/todo-write.md +1 -1
  187. package/src/prompts/tools/vim.md +1 -0
  188. package/src/prompts/tools/web-search.md +2 -0
  189. package/src/prompts/tools/write.md +8 -2
  190. package/src/sdk.ts +68 -30
  191. package/src/session/agent-session.ts +734 -107
  192. package/src/session/file-mention-pruning.ts +64 -0
  193. package/src/session/messages.ts +4 -0
  194. package/src/session/session-manager.ts +59 -2
  195. package/src/skill-state/active-state.ts +141 -5
  196. package/src/skill-state/deep-interview-mutation-guard.ts +67 -3
  197. package/src/slash-commands/builtin-registry.ts +143 -1
  198. package/src/system-prompt.ts +57 -16
  199. package/src/task/executor.ts +125 -20
  200. package/src/task/fork-context-advisory.ts +2 -7
  201. package/src/task/fork-context-budget.ts +9 -0
  202. package/src/task/index.ts +4 -3
  203. package/src/task/output-manager.ts +4 -2
  204. package/src/task/token-log.test.ts +188 -0
  205. package/src/task/token-log.ts +179 -0
  206. package/src/tools/bisect.ts +367 -0
  207. package/src/tools/index.ts +3 -0
  208. package/src/tools/output-meta.ts +116 -4
  209. package/src/tools/search.ts +8 -2
  210. package/src/tools/skill.ts +15 -0
  211. package/src/utils/file-mentions.ts +39 -7
  212. package/src/utils/git.ts +73 -0
  213. package/src/utils/pasted-image-path.ts +170 -0
  214. package/src/web/scrapers/youtube.ts +24 -12
@@ -16,6 +16,7 @@ import { ToolExecutionComponent } from "../../modes/components/tool-execution";
16
16
  import { TtsrNotificationComponent } from "../../modes/components/ttsr-notification";
17
17
  import { getSymbolTheme, theme } from "../../modes/theme/theme";
18
18
  import type { InteractiveModeContext, TodoPhase } from "../../modes/types";
19
+ import { completionNotifyDisabledByEnv } from "../../notifications/config";
19
20
  import { summaryFromMessage } from "../../notifications/helpers";
20
21
  import type { PlanApprovalDetails } from "../../plan-mode/approved-plan";
21
22
  import type { AgentSessionEvent } from "../../session/agent-session";
@@ -23,11 +24,22 @@ import { isSilentAbort, readPendingDisplayTag } from "../../session/messages";
23
24
  import type { ResolveToolDetails } from "../../tools/resolve";
24
25
  import { interruptHint } from "../shared";
25
26
  import { buildAbortDisplayMessage } from "../utils/abort-message";
27
+ import { consumeInjectedOptimisticSignature } from "../utils/injected-user-submission";
26
28
  import { ringTerminalBell } from "../utils/terminal-bell";
29
+ import { argsWithPartialJson } from "../utils/ui-helpers";
27
30
 
28
31
  type AgentSessionEventKind = AgentSessionEvent["type"];
29
32
 
30
33
  const IRC_MESSAGE_VISIBLE_TTL_MS = 10_000;
34
+
35
+ /** Test-only performance counters for advisory baseline tests. */
36
+ export const __eventControllerPerfCounters = {
37
+ messageUpdateContentVisits: 0,
38
+ reset(): void {
39
+ this.messageUpdateContentVisits = 0;
40
+ },
41
+ };
42
+
31
43
  const COMPLETION_NOTIFY_COMMAND_TIMEOUT_MS = 10_000;
32
44
 
33
45
  interface CompletionNotifyPayload {
@@ -100,6 +112,8 @@ export class EventController {
100
112
  #lastAssistantComponent: AssistantMessageComponent | undefined = undefined;
101
113
  #idleCompactionTimer?: NodeJS.Timeout;
102
114
  #ircExpiryTimers = new Map<string, NodeJS.Timeout>();
115
+ #toolIntentCache = new Map<string, { args: unknown; intent: string | undefined }>();
116
+ #thinkingContentIndices = new Set<number>();
103
117
  #handlers: AgentSessionEventHandlers;
104
118
 
105
119
  constructor(private ctx: InteractiveModeContext) {
@@ -279,12 +293,17 @@ export class EventController {
279
293
  const signature = `${textContent}\u0000${imageCount}`;
280
294
 
281
295
  this.#resetReadGroup();
282
- const wasOptimistic = this.ctx.optimisticUserMessageSignature === signature;
296
+ const wasSingleOptimistic = this.ctx.optimisticUserMessageSignature === signature;
297
+ const wasInjectedOptimistic = !wasSingleOptimistic && consumeInjectedOptimisticSignature(this.ctx, signature);
298
+ const wasOptimistic = wasSingleOptimistic || wasInjectedOptimistic;
283
299
  const wasLocallySubmitted = this.ctx.locallySubmittedUserSignatures.delete(signature) || wasOptimistic;
284
300
  if (!wasOptimistic) {
285
301
  this.ctx.addMessageToChat(event.message);
286
302
  }
287
- if (wasOptimistic) {
303
+ // Only clear the single local slot when IT matched; an injected match is
304
+ // consumed from the counting Map above and must not clobber a coexisting
305
+ // pending local optimistic signature.
306
+ if (wasSingleOptimistic) {
288
307
  this.ctx.optimisticUserMessageSignature = undefined;
289
308
  }
290
309
 
@@ -307,12 +326,14 @@ export class EventController {
307
326
  } else if (event.message.role === "assistant") {
308
327
  this.#lastThinkingCount = 0;
309
328
  this.#resetReadGroup();
329
+ this.#toolIntentCache.clear();
330
+ this.#thinkingContentIndices.clear();
310
331
  this.ctx.streamingComponent = new AssistantMessageComponent(undefined, this.ctx.hideThinkingBlock, () =>
311
332
  this.ctx.ui.requestRender(),
312
333
  );
313
334
  this.ctx.streamingMessage = event.message;
314
335
  this.ctx.chatContainer.addChild(this.ctx.streamingComponent);
315
- this.ctx.streamingComponent.updateContent(this.ctx.streamingMessage);
336
+ this.ctx.streamingComponent.updateContent(this.ctx.streamingMessage, { streaming: true });
316
337
  this.ctx.ui.requestRender();
317
338
  }
318
339
  }
@@ -375,46 +396,53 @@ export class EventController {
375
396
  async #handleMessageUpdate(event: Extract<AgentSessionEvent, { type: "message_update" }>): Promise<void> {
376
397
  if (this.ctx.streamingComponent && event.message.role === "assistant") {
377
398
  this.ctx.streamingMessage = event.message;
378
- this.ctx.streamingComponent.updateContent(this.ctx.streamingMessage);
379
-
380
- const thinkingCount = this.ctx.streamingMessage.content.filter(
381
- content => content.type === "thinking" && content.thinking.trim(),
382
- ).length;
383
- if (thinkingCount > this.#lastThinkingCount) {
384
- this.#resetReadGroup();
385
- this.#lastThinkingCount = thinkingCount;
399
+ this.ctx.streamingComponent.updateContent(this.ctx.streamingMessage, { streaming: true });
400
+ const contentIndex = event.assistantMessageEvent?.contentIndex;
401
+ const changedContent =
402
+ typeof contentIndex === "number" && contentIndex >= 0
403
+ ? this.ctx.streamingMessage.content[contentIndex]
404
+ : undefined;
405
+ const contentsToProcess = changedContent ? [changedContent] : this.ctx.streamingMessage.content;
406
+
407
+ if (changedContent?.type === "thinking" && changedContent.thinking.trim()) {
408
+ if (typeof contentIndex === "number") this.#thinkingContentIndices.add(contentIndex);
409
+ const thinkingCount = this.#thinkingContentIndices.size;
410
+ if (thinkingCount > this.#lastThinkingCount) {
411
+ this.#resetReadGroup();
412
+ this.#lastThinkingCount = thinkingCount;
413
+ }
414
+ } else if (!changedContent) {
415
+ const thinkingCount = this.ctx.streamingMessage.content.filter(
416
+ content => content.type === "thinking" && content.thinking.trim(),
417
+ ).length;
418
+ if (thinkingCount > this.#lastThinkingCount) {
419
+ this.#resetReadGroup();
420
+ this.#lastThinkingCount = thinkingCount;
421
+ }
386
422
  }
387
423
 
388
- for (const content of this.ctx.streamingMessage.content) {
424
+ for (const content of contentsToProcess) {
425
+ __eventControllerPerfCounters.messageUpdateContentVisits += 1;
389
426
  if (content.type !== "toolCall") continue;
390
427
  if (content.name === "read") {
391
- if (!readArgsHaveTarget(content.arguments)) {
392
- // Args still streaming — defer until path is parseable so we can route to the
393
- // read group (regular files) vs ToolExecutionComponent (internal URLs).
394
- // Creating either component now would lock the read into the wrong shape.
395
- continue;
396
- }
428
+ if (!readArgsHaveTarget(content.arguments)) continue;
397
429
  if (!readArgsTargetInternalUrl(content.arguments)) {
398
430
  this.#trackReadToolCall(content.id, content.arguments);
399
431
  const component = this.ctx.pendingTools.get(content.id);
400
- if (component) {
401
- component.updateArgs(content.arguments, content.id);
402
- } else {
432
+ if (component) component.updateArgs(content.arguments, content.id);
433
+ else {
403
434
  const group = this.#getReadGroup();
404
435
  group.updateArgs(content.arguments, content.id);
405
436
  this.ctx.pendingTools.set(content.id, group);
406
437
  }
407
438
  continue;
408
439
  }
409
- // Internal URL read falls through to ToolExecutionComponent below.
410
440
  }
411
441
 
412
- // Preserve the raw partial JSON for renderers that need to surface fields before the JSON object closes.
413
- // Bash uses this to show inline env assignments during streaming instead of popping them in at completion.
414
- const renderArgs =
415
- "partialJson" in content
416
- ? { ...content.arguments, __partialJson: content.partialJson }
417
- : content.arguments;
442
+ const renderArgs = argsWithPartialJson(
443
+ content.arguments,
444
+ "partialJson" in content ? content.partialJson : undefined,
445
+ );
418
446
  if (!this.ctx.pendingTools.has(content.id)) {
419
447
  this.#resetReadGroup();
420
448
  this.ctx.chatContainer.addChild(new Text("", 0, 0));
@@ -437,32 +465,30 @@ export class EventController {
437
465
  this.ctx.chatContainer.addChild(component);
438
466
  this.ctx.pendingTools.set(content.id, component);
439
467
  } else {
440
- const component = this.ctx.pendingTools.get(content.id);
441
- if (component) {
442
- component.updateArgs(renderArgs, content.id);
443
- }
468
+ this.ctx.pendingTools.get(content.id)?.updateArgs(renderArgs, content.id);
444
469
  }
445
- }
446
470
 
447
- // Update working message with intent from streamed tool arguments
448
- for (const content of this.ctx.streamingMessage.content) {
449
- if (content.type !== "toolCall") continue;
450
471
  const args = content.arguments;
451
472
  if (!args || typeof args !== "object") continue;
473
+ let intent: string | undefined;
452
474
  if (INTENT_FIELD in args) {
453
- this.#updateWorkingMessageFromIntent(args[INTENT_FIELD]);
454
- continue;
455
- }
456
- const tool = this.ctx.session.getToolByName(content.name);
457
- if (typeof tool?.intent !== "function") continue;
458
- try {
459
- const derived = tool.intent(args as never)?.trim();
460
- if (derived) {
461
- this.#updateWorkingMessageFromIntent(derived);
475
+ intent = String(args[INTENT_FIELD]);
476
+ } else {
477
+ const cached = this.#toolIntentCache.get(content.id);
478
+ if (cached?.args === args) intent = cached.intent;
479
+ else {
480
+ const tool = this.ctx.session.getToolByName(content.name);
481
+ if (typeof tool?.intent === "function") {
482
+ try {
483
+ intent = tool.intent(args as never)?.trim();
484
+ } catch {
485
+ // intent function must never break the UI
486
+ }
487
+ }
488
+ this.#toolIntentCache.set(content.id, { args, intent });
462
489
  }
463
- } catch {
464
- // intent function must never break the UI
465
490
  }
491
+ if (intent) this.#updateWorkingMessageFromIntent(intent);
466
492
  }
467
493
 
468
494
  this.ctx.ui.requestRender();
@@ -495,9 +521,9 @@ export class EventController {
495
521
  // display only — does NOT mutate the persisted message's stopReason
496
522
  // (the marker on errorMessage drives replay-side suppression).
497
523
  const msgWithoutAbort = { ...this.ctx.streamingMessage, stopReason: "stop" as const };
498
- this.ctx.streamingComponent.updateContent(msgWithoutAbort);
524
+ this.ctx.streamingComponent.updateContent(msgWithoutAbort, { streaming: false });
499
525
  } else {
500
- this.ctx.streamingComponent.updateContent(this.ctx.streamingMessage);
526
+ this.ctx.streamingComponent.updateContent(this.ctx.streamingMessage, { streaming: false });
501
527
  }
502
528
 
503
529
  if (this.ctx.streamingMessage.stopReason !== "aborted" && this.ctx.streamingMessage.stopReason !== "error") {
@@ -941,6 +967,8 @@ export class EventController {
941
967
  }
942
968
 
943
969
  sendCompletionNotification(): void {
970
+ // Per-run hard opt-out (env, config-untouched, child-inheritable): GJC_NOTIFY=off.
971
+ if (completionNotifyDisabledByEnv(process.env)) return;
944
972
  const isBackgrounded = this.ctx.isBackgrounded !== false;
945
973
  const notify = settings.get("completion.notify");
946
974
  if (notify === "off") return;
@@ -23,6 +23,7 @@ import { HookSelectorComponent } from "../../modes/components/hook-selector";
23
23
  import { getAvailableThemesWithPaths, getThemeByName, setTheme, type Theme, theme } from "../../modes/theme/theme";
24
24
  import type { InteractiveModeContext } from "../../modes/types";
25
25
  import { setSessionTerminalTitle, setTerminalTitle } from "../../utils/title-generator";
26
+ import { applyInjectedUserSubmission } from "../utils/injected-user-submission";
26
27
  import { classifyHookSelectorBellEvent, ringTerminalBell } from "../utils/terminal-bell";
27
28
 
28
29
  const MAX_WIDGET_LINES = 10;
@@ -954,7 +955,13 @@ export class ExtensionUiController {
954
955
  }
955
956
 
956
957
  #sendExtensionUserMessage: SendUserMessageHandler = (content, options) => {
957
- this.ctx.session.sendUserMessage(content, options).catch((err: unknown) => {
958
+ // Compute queued BEFORE send: prompt() may flip session.isStreaming synchronously.
959
+ const queued = Boolean(options?.deliverAs) || this.ctx.session.isStreaming;
960
+ // Call send first so the busy/queued path finds the session queue populated
961
+ // (queueSteer/queueFollowUp push synchronously) before refreshing pending display.
962
+ const send = this.ctx.session.sendUserMessage(content, options);
963
+ applyInjectedUserSubmission(this.ctx, { content, queued });
964
+ send.catch((err: unknown) => {
958
965
  this.ctx.showError(`Extension sendUserMessage failed: ${err instanceof Error ? err.message : String(err)}`);
959
966
  });
960
967
  };
@@ -18,6 +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
22
  import { generateSessionTitle, setSessionTerminalTitle } from "../../utils/title-generator";
22
23
  import { type QueuedMessageMoveDirection, QueuedMessageSelectorComponent } from "../components/queued-message-selector";
23
24
 
@@ -26,8 +27,6 @@ interface Expandable {
26
27
  }
27
28
 
28
29
  const INTERACTIVE_ABORT_CLEANUP_TIMEOUT_MS = 5_000;
29
- const CLIPBOARD_TEMP_IMAGE_FILE_PATTERN = /^clipboard-\d{4}-\d{2}-\d{2}-\d{6}-[A-Za-z0-9]+\.(?:png|jpe?g|gif|webp)$/i;
30
- const MACOS_CLIPBOARD_TEMP_DIR_PATTERN = /^\/var\/folders\/[^/]+\/[^/]+\/T$/;
31
30
 
32
31
  function isExpandable(obj: unknown): obj is Expandable {
33
32
  return typeof obj === "object" && obj !== null && "setExpanded" in obj && typeof obj.setExpanded === "function";
@@ -1061,10 +1060,15 @@ export class InputController {
1061
1060
  }
1062
1061
 
1063
1062
  handleTextPaste(text: string): boolean | Promise<boolean> {
1064
- const imagePath = this.#getPastedImagePathCandidate(text);
1063
+ const imagePath = resolvePastedImagePath(text, { cwd: this.ctx.sessionManager.getCwd() });
1065
1064
  return imagePath ? this.#attachPastedImagePath(imagePath) : false;
1066
1065
  }
1067
1066
 
1067
+ /**
1068
+ * Returns `false` on every failure path so the editor replays the original
1069
+ * bracketed paste — the raw path text must never be lost when attachment
1070
+ * is impossible (unsupported content, oversized image, load error).
1071
+ */
1068
1072
  async #attachPastedImagePath(imagePath: string): Promise<boolean> {
1069
1073
  try {
1070
1074
  const image = await loadImageInput({
@@ -1073,8 +1077,8 @@ export class InputController {
1073
1077
  autoResize: this.ctx.settings.get("images.autoResize"),
1074
1078
  });
1075
1079
  if (!image) {
1076
- this.ctx.showStatus("Unsupported pasted clipboard image file");
1077
- return true;
1080
+ this.ctx.showStatus("Unsupported pasted image file");
1081
+ return false;
1078
1082
  }
1079
1083
 
1080
1084
  this.ctx.pendingImages.push({
@@ -1089,22 +1093,13 @@ export class InputController {
1089
1093
  } catch (error) {
1090
1094
  if (error instanceof ImageInputTooLargeError) {
1091
1095
  this.ctx.showStatus(error.message);
1092
- return true;
1096
+ return false;
1093
1097
  }
1094
- this.ctx.showStatus("Failed to attach pasted clipboard image");
1095
- return true;
1098
+ this.ctx.showStatus("Failed to attach pasted image");
1099
+ return false;
1096
1100
  }
1097
1101
  }
1098
1102
 
1099
- #getPastedImagePathCandidate(text: string): string | undefined {
1100
- const resolvedPath = path.resolve(text.trim());
1101
- const parentDir = path.dirname(resolvedPath);
1102
- const isClipboardTempPath =
1103
- (parentDir === "/tmp" || MACOS_CLIPBOARD_TEMP_DIR_PATTERN.test(parentDir)) &&
1104
- CLIPBOARD_TEMP_IMAGE_FILE_PATTERN.test(path.basename(resolvedPath));
1105
- return isClipboardTempPath ? resolvedPath : undefined;
1106
- }
1107
-
1108
1103
  #nextImagePlaceholder(): string {
1109
1104
  return `[image ${this.ctx.pendingImages.length}]`;
1110
1105
  }
@@ -1308,7 +1303,7 @@ export class InputController {
1308
1303
  // If streaming, re-add the streaming component with updated visibility and re-render
1309
1304
  if (this.ctx.streamingComponent && this.ctx.streamingMessage) {
1310
1305
  this.ctx.streamingComponent.setHideThinkingBlock(this.ctx.hideThinkingBlock);
1311
- this.ctx.streamingComponent.updateContent(this.ctx.streamingMessage);
1306
+ this.ctx.streamingComponent.updateContent(this.ctx.streamingMessage, { streaming: true });
1312
1307
  this.ctx.chatContainer.addChild(this.ctx.streamingComponent);
1313
1308
  }
1314
1309
 
@@ -88,6 +88,7 @@ import { SessionSelectorComponent } from "../components/session-selector";
88
88
  import { SettingsSelectorComponent } from "../components/settings-selector";
89
89
  import type { StatusLineSettings } from "../components/status-line";
90
90
  import { ThemeSelectorComponent } from "../components/theme-selector";
91
+ import { ThinkingSelectorComponent } from "../components/thinking-selector";
91
92
  import { ToolExecutionComponent } from "../components/tool-execution";
92
93
  import { TreeSelectorComponent } from "../components/tree-selector";
93
94
  import { UserMessageSelectorComponent } from "../components/user-message-selector";
@@ -431,6 +432,46 @@ export class SelectorController {
431
432
  });
432
433
  }
433
434
 
435
+ showEffortSelector(): void {
436
+ const availableLevels = [
437
+ ThinkingLevel.Inherit,
438
+ ThinkingLevel.Off,
439
+ ...this.ctx.session.getAvailableThinkingLevels(),
440
+ ];
441
+
442
+ this.showSelector(done => {
443
+ const selector = new ThinkingSelectorComponent(
444
+ this.ctx.session.thinkingLevel,
445
+ availableLevels,
446
+ level => {
447
+ done();
448
+
449
+ const configuredDefault = this.ctx.settings.get("defaultThinkingLevel");
450
+ const levelToApply = level === ThinkingLevel.Inherit ? configuredDefault : level;
451
+ this.ctx.session.setThinkingLevel(levelToApply, false);
452
+ const effectiveLevel = this.ctx.session.thinkingLevel ?? ThinkingLevel.Off;
453
+ const requestedLabel =
454
+ level === ThinkingLevel.Inherit ? `${level} (configured default: ${configuredDefault})` : level;
455
+ const clampedSuffix =
456
+ effectiveLevel === levelToApply ? "" : ` Requested ${levelToApply}; effective ${effectiveLevel}.`;
457
+
458
+ this.ctx.statusLine.invalidate();
459
+ this.ctx.updateEditorBorderColor();
460
+ this.ctx.updateEditorTopBorder();
461
+ this.ctx.ui.requestRender();
462
+ this.ctx.showStatus(
463
+ `Reasoning effort set to ${requestedLabel}. Effective effort: ${effectiveLevel}.${clampedSuffix}`,
464
+ );
465
+ },
466
+ () => {
467
+ done();
468
+ this.ctx.ui.requestRender();
469
+ },
470
+ );
471
+ return { component: selector, focus: selector.getSelectList() };
472
+ });
473
+ }
474
+
434
475
  showSettingsSelector(): void {
435
476
  getAvailableThemes().then(availableThemes => {
436
477
  this.showSelector(done => {
@@ -118,7 +118,7 @@ import { UiHelpers } from "./utils/ui-helpers";
118
118
 
119
119
  const INTERACTIVE_ABORT_CLEANUP_TIMEOUT_MS = 5_000;
120
120
  const COMPOSER_NEWLINE_HINT = process.platform === "win32" ? "Alt+Enter/Ctrl+J" : "Shift+Enter/Ctrl+J";
121
- const DEFAULT_COMPOSER_PLACEHOLDER = `Type your message... ${COMPOSER_NEWLINE_HINT}: New line · Ctrl+C: Clear · Shift+Tab: Reasoning`;
121
+ export const DEFAULT_COMPOSER_PLACEHOLDER = `Type your message... ${COMPOSER_NEWLINE_HINT}: New line · Ctrl+C: Clear · Ctrl+R: Search history · Shift+Tab: Reasoning`;
122
122
  const WELCOME_RESERVED_CONTAINER_CHILD_LIMIT = 8;
123
123
  const FRIENDLY_KEY_PARTS: Record<string, string> = {
124
124
  alt: "Alt",
@@ -163,6 +163,7 @@ function configureDefaultComposerChrome(editor: CustomEditor): void {
163
163
  editor.setInputPrefix(getDefaultInputPrefix());
164
164
  editor.setPlaceholder(DEFAULT_COMPOSER_PLACEHOLDER);
165
165
  editor.setPaddingX(1);
166
+ editor.setRightGutterWidth(1);
166
167
  editor.setTopBorder(undefined);
167
168
  }
168
169
 
@@ -329,6 +330,7 @@ export class InteractiveMode implements InteractiveModeContext {
329
330
  onInputCallback?: (input: SubmittedUserInput) => void;
330
331
  optimisticUserMessageSignature: string | undefined = undefined;
331
332
  locallySubmittedUserSignatures: Set<string> = new Set();
333
+ optimisticInjectedSignatures: Map<string, number> = new Map();
332
334
  #pendingSubmittedInput: SubmittedUserInput | undefined;
333
335
  #pendingSubmissionDispose: (() => void) | undefined;
334
336
  lastSigintTime = 0;
@@ -1715,6 +1717,7 @@ export class InteractiveMode implements InteractiveModeContext {
1715
1717
  planContent,
1716
1718
  finalPlanFilePath: options.finalPlanFilePath,
1717
1719
  contextPreserved: options.preserveContext === true,
1720
+ tools: this.session.getActiveToolNames(),
1718
1721
  });
1719
1722
  await this.session.prompt(planModePrompt, { synthetic: true });
1720
1723
  }
@@ -2206,6 +2209,7 @@ export class InteractiveMode implements InteractiveModeContext {
2206
2209
  message => renderWorkingMessage(message, this.#getWorkingMessageAccent()),
2207
2210
  this.#defaultWorkingMessage,
2208
2211
  getSymbolTheme().spinnerFrames,
2212
+ { timeDependentColor: true },
2209
2213
  );
2210
2214
  this.statusContainer.addChild(this.loadingAnimation);
2211
2215
  }
@@ -2363,6 +2367,11 @@ export class InteractiveMode implements InteractiveModeContext {
2363
2367
  return this.#commandController.handleClearCommand();
2364
2368
  }
2365
2369
 
2370
+ handleContextClearCommand(): Promise<void> {
2371
+ this.#prepareSessionSwitch();
2372
+ return this.#commandController.handleContextClearCommand();
2373
+ }
2374
+
2366
2375
  handleDropCommand(): Promise<void> {
2367
2376
  this.#prepareSessionSwitch();
2368
2377
  return this.#commandController.handleDropCommand();
@@ -2552,6 +2561,10 @@ export class InteractiveMode implements InteractiveModeContext {
2552
2561
  this.#selectorController.showModelSelector(options);
2553
2562
  }
2554
2563
 
2564
+ showEffortSelector(): void {
2565
+ this.#selectorController.showEffortSelector();
2566
+ }
2567
+
2555
2568
  showProviderOnboarding(): void {
2556
2569
  this.#selectorController.showProviderOnboarding();
2557
2570
  }
@@ -22,8 +22,13 @@ import { type Theme, theme } from "../../modes/theme/theme";
22
22
  import type { AgentSession } from "../../session/agent-session";
23
23
  import { initializeExtensions } from "../runtime-init";
24
24
  import { dispatchRpcCommand } from "../shared/agent-wire/command-dispatch";
25
- import { AgentWireFrameSequencer, toAgentWireEventFrame } from "../shared/agent-wire/event-envelope";
26
- import { rpcError as error } from "../shared/agent-wire/responses";
25
+ import {
26
+ AgentWireCompactEventEncoder,
27
+ AgentWireFrameSequencer,
28
+ toAgentWireCompactEventFrame,
29
+ toAgentWireEventFrame,
30
+ } from "../shared/agent-wire/event-envelope";
31
+ import { rpcError as error, rpcSuccess } from "../shared/agent-wire/responses";
27
32
  import { registerRpcSession, unregisterRpcSession } from "../shared/agent-wire/session-registry";
28
33
  import { defaultAuditPath, UnattendedAuditLog } from "../shared/agent-wire/unattended-audit";
29
34
  import { modelSupportsTokenCostMetrics, UnattendedSessionControlPlane } from "../shared/agent-wire/unattended-session";
@@ -168,15 +173,9 @@ function auditOutcomeFor(event: string): "accepted" | "rejected" | "denied" | "e
168
173
  return "info";
169
174
  }
170
175
 
171
- export class RpcListenRefusedError extends Error {
172
- constructor(socketPath: string) {
173
- super(
174
- `RPC --listen refused: a live server is already listening on ${socketPath}. ` +
175
- "Stop it first or choose a different --listen path.",
176
- );
177
- this.name = "RpcListenRefusedError";
178
- }
179
- }
176
+ // Thrown by prepareRpcSocketPath when the --listen path has a live owner;
177
+ // re-exported here because main.ts catches it at the RPC launch boundary.
178
+ export { RpcListenRefusedError } from "./rpc-socket-security";
180
179
 
181
180
  /**
182
181
  * Probe whether a unix-domain socket path has a live server accepting
@@ -297,6 +296,7 @@ export async function runRpcMode(
297
296
  output({ type: "ready" });
298
297
  }
299
298
  const emitRpcTitles = shouldEmitRpcTitles();
299
+ const rpcCapabilities = { compactMessageUpdate: false };
300
300
  const decodeError = (err: unknown): string => (err instanceof Error ? err.message : String(err));
301
301
 
302
302
  const pendingExtensionRequests = new Map<string, PendingExtensionRequest>();
@@ -636,8 +636,13 @@ export async function runRpcMode(
636
636
  // Output all agent events as canonical agent-wire `event` frames (docs/rpc.md):
637
637
  // { type:"event", protocol_version, session_id, seq, frame_id, payload:{ event_type, event } }.
638
638
  const eventSequencer = new AgentWireFrameSequencer(session.sessionId);
639
+ const compactEventEncoder = new AgentWireCompactEventEncoder(eventSequencer);
639
640
  session.subscribe(event => {
640
- output(toAgentWireEventFrame(event, eventSequencer));
641
+ output(
642
+ rpcCapabilities.compactMessageUpdate
643
+ ? toAgentWireCompactEventFrame(event, compactEventEncoder)
644
+ : toAgentWireEventFrame(event, eventSequencer),
645
+ );
641
646
  });
642
647
 
643
648
  // Handle a single command through the shared agent-wire dispatcher so RPC
@@ -710,6 +715,21 @@ export async function runRpcMode(
710
715
  hostUriBridge.handleResult(parsed);
711
716
  return;
712
717
  }
718
+ if ((parsed as RpcCommand).type === "set_capabilities") {
719
+ // RPC has no startup handshake; clients negotiate optional event-shape capabilities
720
+ // with this explicit command. Defaults remain legacy/full-frame until accepted here.
721
+ const command = parsed as Extract<RpcCommand, { type: "set_capabilities" }>;
722
+ const requested = Array.isArray(command.capabilities) ? command.capabilities : [];
723
+ const acceptedCapabilities = requested.filter(capability => capability === "compact_message_update");
724
+ rpcCapabilities.compactMessageUpdate = acceptedCapabilities.includes("compact_message_update");
725
+ output(
726
+ rpcSuccess(command.id, "set_capabilities", {
727
+ acceptedCapabilities,
728
+ unsupported: requested.filter(capability => capability !== "compact_message_update"),
729
+ }),
730
+ );
731
+ return;
732
+ }
713
733
  // Ordered commands run through a serial chain to preserve causal order; the
714
734
  // reader never blocks, so cancellation commands stay responsive even while a
715
735
  // long command is in flight (issue 13).
@@ -9,6 +9,18 @@ export class RpcSocketSecurityError extends Error {
9
9
  }
10
10
  }
11
11
 
12
+ // The launch boundary in main.ts catches exactly this class to turn a
13
+ // duplicate --listen into a clean stderr diagnostic + non-zero exit (issue 19).
14
+ export class RpcListenRefusedError extends Error {
15
+ constructor(socketPath: string) {
16
+ super(
17
+ `RPC --listen refused: a live server is already listening on ${socketPath}. ` +
18
+ "Stop it first or choose a different --listen path.",
19
+ );
20
+ this.name = "RpcListenRefusedError";
21
+ }
22
+ }
23
+
12
24
  const unsafeBits = 0o077;
13
25
 
14
26
  function currentUid(): number | undefined {
@@ -53,7 +65,7 @@ export async function prepareRpcSocketPath(socketPath: string): Promise<void> {
53
65
  if (!existing.isSocket()) throw new RpcSocketSecurityError(`RPC socket path is not a socket: ${socketPath}`);
54
66
  assertPrivateMode(existing.mode, socketPath);
55
67
  if (await probeUnixSocketAlive(socketPath)) {
56
- throw new RpcSocketSecurityError(`RPC socket path is live: ${socketPath}`);
68
+ throw new RpcListenRefusedError(socketPath);
57
69
  }
58
70
  await fs.unlink(socketPath);
59
71
  }
@@ -13,6 +13,7 @@ import type { SessionStats } from "../../session/agent-session";
13
13
  import type { TodoPhase } from "../../tools/todo-write";
14
14
 
15
15
  export type RpcGetStateInclude = "tools" | "dumpTools" | "systemPrompt";
16
+ export type RpcCapability = "compact_message_update";
16
17
 
17
18
  // ============================================================================
18
19
  // RPC Commands (stdin)
@@ -33,6 +34,7 @@ export type RpcCommand =
33
34
  | { id?: string; type: "set_host_tools"; tools: RpcHostToolDefinition[] }
34
35
  | { id?: string; type: "set_host_uri_schemes"; schemes: RpcHostUriSchemeDefinition[] }
35
36
  | { id?: string; type: "get_pending_workflow_gates" }
37
+ | { id?: string; type: "set_capabilities"; capabilities: RpcCapability[] }
36
38
 
37
39
  // Model
38
40
  | { id?: string; type: "set_model"; provider: string; modelId: string }
@@ -140,6 +142,13 @@ export type RpcResponse =
140
142
  success: true;
141
143
  data: { gates: RpcWorkflowGate[] };
142
144
  }
145
+ | {
146
+ id?: string;
147
+ type: "response";
148
+ command: "set_capabilities";
149
+ success: true;
150
+ data: { acceptedCapabilities: RpcCapability[]; unsupported: string[] };
151
+ }
143
152
 
144
153
  // Model
145
154
  | {
@@ -130,6 +130,10 @@ export function isRpcCommand(value: unknown): value is RpcCommand {
130
130
  return Array.isArray(value.tools) && value.tools.every(hostToolDefinition);
131
131
  case "set_host_uri_schemes":
132
132
  return Array.isArray(value.schemes) && value.schemes.every(hostUriScheme);
133
+ case "set_capabilities":
134
+ return (
135
+ Array.isArray(value.capabilities) && value.capabilities.every(capability => typeof capability === "string")
136
+ );
133
137
  case "set_model":
134
138
  return stringField(value, "provider") && stringField(value, "modelId");
135
139
  case "set_thinking_level":
@@ -13,6 +13,8 @@
13
13
  * an `AgentSessionEvent` variant fails typecheck until it is registered, and so
14
14
  * conformance tests can assert fixture coverage equals the registry exactly.
15
15
  */
16
+
17
+ import type { AssistantMessageEvent } from "@gajae-code/ai";
16
18
  import type { AgentSessionEvent } from "../../../session/agent-session";
17
19
 
18
20
  /** Wire protocol version. Bump on breaking envelope/semantic changes. */
@@ -68,6 +70,25 @@ export interface AgentWireEventPayload {
68
70
  event_type: AgentWireEventType;
69
71
  event: AgentSessionEvent;
70
72
  }
73
+ export type AgentWireCompactAssistantMessageEvent = AssistantMessageEvent extends infer TEvent
74
+ ? TEvent extends { type: string }
75
+ ? Omit<TEvent, "partial" | "message" | "error">
76
+ : never
77
+ : never;
78
+
79
+ export interface AgentWireCompactMessageUpdatePayload {
80
+ event_type: "message_update";
81
+ /** Compact RPC opt-in payload: delta-only update plus minimal routing metadata. */
82
+ compact: true;
83
+ message_id: string | null;
84
+ content_index: number | null;
85
+ assistantMessageEvent: AgentWireCompactAssistantMessageEvent;
86
+ /** Full message checkpoint, snapshotted synchronously when this frame is enqueued. */
87
+ checkpoint_message?: Extract<AgentSessionEvent, { type: "message_update" }>["message"];
88
+ checkpoint_reason?: "periodic";
89
+ }
90
+
91
+ export type AgentWireEventFramePayload = AgentWireEventPayload | AgentWireCompactMessageUpdatePayload;
71
92
 
72
93
  /**
73
94
  * Bounded observed-signal vocabulary surfaced to owner control planes. Mirrors
@@ -146,3 +167,4 @@ export interface AgentWireFrameEnvelope<TType extends AgentWireFrameType = Agent
146
167
 
147
168
  /** An `AgentSessionEvent` serialized into a versioned wire frame. */
148
169
  export type AgentWireEventFrame = AgentWireFrameEnvelope<"event", AgentWireEventPayload>;
170
+ export type AgentWireCompactEventFrame = AgentWireFrameEnvelope<"event", AgentWireEventFramePayload>;