@gajae-code/coding-agent 0.9.1 → 0.9.3

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 (123) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/dist/types/capability/mcp.d.ts +3 -1
  3. package/dist/types/cli/args.d.ts +1 -0
  4. package/dist/types/cli/notify-cli.d.ts +5 -0
  5. package/dist/types/cli.d.ts +3 -0
  6. package/dist/types/commands/launch.d.ts +3 -0
  7. package/dist/types/commands/mcp.d.ts +1 -1
  8. package/dist/types/config/model-profile-activation.d.ts +7 -7
  9. package/dist/types/config/model-registry.d.ts +7 -3
  10. package/dist/types/config/model-resolver.d.ts +2 -2
  11. package/dist/types/config/settings-schema.d.ts +1 -1
  12. package/dist/types/config/settings.d.ts +7 -0
  13. package/dist/types/extensibility/runtime-skill-discovery.d.ts +20 -0
  14. package/dist/types/gjc-runtime/team-runtime.d.ts +9 -0
  15. package/dist/types/modes/components/settings-selector.d.ts +2 -2
  16. package/dist/types/modes/components/status-line/types.d.ts +6 -0
  17. package/dist/types/modes/components/status-line.d.ts +1 -0
  18. package/dist/types/modes/components/thinking-selector.d.ts +6 -1
  19. package/dist/types/modes/types.d.ts +1 -0
  20. package/dist/types/modes/utils/ui-helpers.d.ts +12 -0
  21. package/dist/types/notifications/config-commands.d.ts +29 -0
  22. package/dist/types/notifications/index.d.ts +12 -1
  23. package/dist/types/notifications/telegram-daemon-cli.d.ts +3 -3
  24. package/dist/types/notifications/threaded-render.d.ts +2 -0
  25. package/dist/types/notifications/topic-registry.d.ts +4 -5
  26. package/dist/types/runtime-credential-selector.d.ts +7 -0
  27. package/dist/types/runtime-mcp/config-writer.d.ts +4 -3
  28. package/dist/types/runtime-mcp/types.d.ts +5 -4
  29. package/dist/types/sdk.d.ts +7 -1
  30. package/dist/types/session/agent-session.d.ts +22 -3
  31. package/dist/types/session/session-manager.d.ts +1 -0
  32. package/dist/types/tools/index.d.ts +1 -0
  33. package/dist/types/tools/skill-discovery.d.ts +40 -0
  34. package/dist/types/tools/skill.d.ts +9 -8
  35. package/dist/types/utils/linkedom.d.ts +4 -0
  36. package/dist/types/utils/pasted-image-path.d.ts +5 -3
  37. package/package.json +11 -7
  38. package/scripts/build-binary.ts +2 -40
  39. package/scripts/compile-args.ts +91 -0
  40. package/scripts/generate-docs-index.ts +1 -0
  41. package/src/capability/mcp.ts +3 -1
  42. package/src/cli/args.ts +8 -0
  43. package/src/cli/mcp-cli.ts +108 -26
  44. package/src/cli/notify-cli.ts +27 -1
  45. package/src/cli.ts +26 -0
  46. package/src/commands/launch.ts +5 -0
  47. package/src/commands/mcp.ts +8 -8
  48. package/src/commands/notify.ts +2 -2
  49. package/src/commands/stats.ts +6 -2
  50. package/src/config/mcp-schema.json +21 -17
  51. package/src/config/model-profile-activation.ts +23 -7
  52. package/src/config/model-registry.ts +21 -4
  53. package/src/config/model-resolver.ts +5 -16
  54. package/src/config/settings-schema.ts +3 -2
  55. package/src/config/settings.ts +24 -1
  56. package/src/coordinator-mcp/server.ts +7 -4
  57. package/src/discovery/builtin.ts +14 -0
  58. package/src/discovery/mcp-json.ts +14 -0
  59. package/src/extensibility/runtime-skill-discovery.ts +229 -0
  60. package/src/gjc-runtime/launch-tmux.ts +72 -12
  61. package/src/gjc-runtime/psmux-detect.ts +17 -2
  62. package/src/gjc-runtime/state-runtime.ts +105 -12
  63. package/src/gjc-runtime/team-runtime.ts +35 -3
  64. package/src/gjc-runtime/workflow-manifest.generated.json +4 -28
  65. package/src/gjc-runtime/workflow-manifest.ts +1 -7
  66. package/src/internal-urls/docs-index.generated.ts +3 -2
  67. package/src/internal-urls/gjc-protocol.ts +14 -1
  68. package/src/main.ts +26 -3
  69. package/src/modes/components/assistant-message.ts +1 -1
  70. package/src/modes/components/settings-selector.ts +3 -3
  71. package/src/modes/components/status-line/presets.ts +5 -7
  72. package/src/modes/components/status-line/segments.ts +14 -0
  73. package/src/modes/components/status-line/types.ts +6 -0
  74. package/src/modes/components/status-line.ts +7 -1
  75. package/src/modes/components/thinking-selector.ts +79 -18
  76. package/src/modes/components/tool-execution.ts +1 -1
  77. package/src/modes/controllers/event-controller.ts +9 -9
  78. package/src/modes/controllers/input-controller.ts +52 -11
  79. package/src/modes/controllers/selector-controller.ts +7 -5
  80. package/src/modes/interactive-mode.ts +12 -4
  81. package/src/modes/types.ts +1 -0
  82. package/src/modes/utils/ui-helpers.ts +202 -47
  83. package/src/notifications/config-commands.ts +70 -0
  84. package/src/notifications/index.ts +156 -9
  85. package/src/notifications/rich-render.ts +1 -0
  86. package/src/notifications/telegram-daemon-cli.ts +13 -5
  87. package/src/notifications/telegram-daemon.ts +87 -4
  88. package/src/notifications/threaded-render.ts +14 -0
  89. package/src/notifications/topic-registry.ts +9 -8
  90. package/src/prompts/tools/skill-discovery.md +13 -0
  91. package/src/prompts/tools/skill.md +2 -2
  92. package/src/runtime-credential-selector.ts +35 -0
  93. package/src/runtime-mcp/config-writer.ts +4 -3
  94. package/src/runtime-mcp/config.ts +1 -0
  95. package/src/runtime-mcp/types.ts +5 -4
  96. package/src/sdk.ts +68 -5
  97. package/src/session/agent-session.ts +159 -21
  98. package/src/session/session-manager.ts +68 -40
  99. package/src/system-prompt.ts +4 -6
  100. package/src/tools/bash-interactive.ts +9 -4
  101. package/src/tools/browser/readable.ts +2 -2
  102. package/src/tools/fetch.ts +5 -5
  103. package/src/tools/index.ts +4 -0
  104. package/src/tools/skill-discovery.ts +73 -0
  105. package/src/tools/skill.ts +29 -13
  106. package/src/utils/linkedom.ts +14 -0
  107. package/src/utils/pasted-image-path.ts +23 -9
  108. package/src/web/scrapers/arxiv.ts +2 -2
  109. package/src/web/scrapers/go-pkg.ts +2 -2
  110. package/src/web/scrapers/iacr.ts +2 -2
  111. package/src/web/scrapers/readthedocs.ts +2 -2
  112. package/src/web/scrapers/twitter.ts +3 -2
  113. package/src/web/scrapers/wikipedia.ts +2 -2
  114. package/src/export/html/template.css +0 -1060
  115. package/src/export/html/template.html +0 -47
  116. package/src/export/html/template.js +0 -2314
  117. package/vendor/insane-search/engine/tests/test_hardening.py +0 -57
  118. package/vendor/insane-search/engine/tests/test_smoke.py +0 -152
  119. package/vendor/insane-search/engine/tests/test_u1.py +0 -200
  120. package/vendor/insane-search/engine/tests/test_u4.py +0 -131
  121. package/vendor/insane-search/engine/tests/test_u5.py +0 -163
  122. package/vendor/insane-search/engine/tests/test_u7.py +0 -124
  123. package/vendor/insane-search/engine/tests/test_u8.py +0 -216
@@ -36,17 +36,27 @@ interface RenderInitialMessagesOptions {
36
36
  preserveExistingChat?: boolean;
37
37
  }
38
38
 
39
+ function cloneRenderArgs(args: Record<string, unknown>): Record<string, unknown> {
40
+ try {
41
+ return structuredClone(args);
42
+ } catch {
43
+ return { ...args };
44
+ }
45
+ }
46
+
39
47
  export function argsWithPartialJson(args: unknown, partialJson: unknown): unknown {
40
48
  if (typeof partialJson !== "string" || !args || typeof args !== "object" || Array.isArray(args)) return args;
41
- // Non-enumerable so the transient streaming buffer reaches renderers via direct
42
- // property read but never serializes into persisted/wire message content.
43
- Object.defineProperty(args, "__partialJson", {
49
+ // Keep the transient streaming buffer on a renderer-only snapshot. The live
50
+ // assistant message args are later validated/executed, so UI-only metadata or
51
+ // renderer mutations must never share that object reference.
52
+ const renderArgs = cloneRenderArgs(args as Record<string, unknown>);
53
+ Object.defineProperty(renderArgs, "__partialJson", {
44
54
  value: partialJson,
45
55
  enumerable: false,
46
56
  configurable: true,
47
57
  writable: true,
48
58
  });
49
- return args;
59
+ return renderArgs;
50
60
  }
51
61
 
52
62
  type QueuedMessages = {
@@ -54,6 +64,127 @@ type QueuedMessages = {
54
64
  followUp: string[];
55
65
  };
56
66
 
67
+ const CHAT_CHILD_CAP = 400;
68
+ const CHAT_COLLAPSE_BATCH_SIZE = 300;
69
+
70
+ const chatChildAddedAt = new WeakMap<Component, number>();
71
+
72
+ export class CollapsedChatHistoryComponent implements Component {
73
+ readonly isCollapsedChatHistory = true;
74
+ count: number;
75
+ startTime: number;
76
+ endTime: number;
77
+
78
+ constructor(count: number, startTime: number, endTime: number) {
79
+ this.count = count;
80
+ this.startTime = startTime;
81
+ this.endTime = endTime;
82
+ }
83
+
84
+ merge(count: number, startTime: number, endTime: number): void {
85
+ this.count += count;
86
+ this.startTime = Math.min(this.startTime, startTime);
87
+ this.endTime = Math.max(this.endTime, endTime);
88
+ }
89
+
90
+ render(_width: number): string[] {
91
+ const label = `[${this.count} earlier messages collapsed — ${formatChatCollapseTime(this.startTime)} to ${formatChatCollapseTime(this.endTime)}]`;
92
+ return [theme.fg("dim", label)];
93
+ }
94
+
95
+ invalidate(): void {}
96
+ }
97
+
98
+ function formatChatCollapseTime(timestamp: number): string {
99
+ return new Date(timestamp).toLocaleTimeString(undefined, {
100
+ hour: "2-digit",
101
+ minute: "2-digit",
102
+ hour12: false,
103
+ });
104
+ }
105
+
106
+ function isCollapsedChatHistory(component: Component): component is CollapsedChatHistoryComponent {
107
+ return component instanceof CollapsedChatHistoryComponent;
108
+ }
109
+
110
+ function isActiveChatChild(ctx: InteractiveModeContext, component: Component): boolean {
111
+ if ([...ctx.pendingTools.values()].includes(component as never)) return true;
112
+ if (ctx.pendingBashComponents.includes(component as never)) return true;
113
+ if (ctx.pendingPythonComponents.includes(component as never)) return true;
114
+ return component === ctx.bashComponent || component === ctx.pythonComponent || component === ctx.streamingComponent;
115
+ }
116
+
117
+ function getChatChildTime(component: Component): number {
118
+ return chatChildAddedAt.get(component) ?? Date.now();
119
+ }
120
+
121
+ export function addChatChild(ctx: InteractiveModeContext, component: Component): void {
122
+ ctx.chatContainer.addChild(component);
123
+ chatChildAddedAt.set(component, Date.now());
124
+ trimChatChildren(ctx);
125
+ }
126
+
127
+ export function trimChatChildren(ctx: InteractiveModeContext): void {
128
+ const children = ctx.chatContainer.children;
129
+
130
+ // Collapse the oldest completed children into history placeholders until the cap is
131
+ // satisfied or no further reduction is structurally possible. Active (pending/running)
132
+ // children and existing placeholders are never collapsed, so a conversation whose
133
+ // top-level children are dominated by active components may legitimately exceed the cap.
134
+ let scanFrom = 0;
135
+ while (children.length > CHAT_CHILD_CAP) {
136
+ // Advance to the oldest collapsible completed child, skipping active children and
137
+ // existing placeholders so the cap is enforced even when the oldest child is active.
138
+ while (
139
+ scanFrom < children.length &&
140
+ (isActiveChatChild(ctx, children[scanFrom]) || isCollapsedChatHistory(children[scanFrom]))
141
+ ) {
142
+ scanFrom++;
143
+ }
144
+ if (scanFrom >= children.length) return; // only active/placeholder children remain
145
+
146
+ // Extend over the oldest contiguous run of completed children (up to the batch size).
147
+ let collapseEndExclusive = scanFrom;
148
+ const maxEnd = Math.min(children.length, scanFrom + CHAT_COLLAPSE_BATCH_SIZE);
149
+ while (
150
+ collapseEndExclusive < maxEnd &&
151
+ !isActiveChatChild(ctx, children[collapseEndExclusive]) &&
152
+ !isCollapsedChatHistory(children[collapseEndExclusive])
153
+ ) {
154
+ collapseEndExclusive++;
155
+ }
156
+ const collapseCount = collapseEndExclusive - scanFrom;
157
+
158
+ const prev = scanFrom > 0 ? children[scanFrom - 1] : undefined;
159
+ const mergeTarget = prev && isCollapsedChatHistory(prev) ? prev : undefined;
160
+ // A lone completed child with no adjacent placeholder to merge into cannot reduce
161
+ // the child count (collapsing it yields a 1-count placeholder). Skip past it.
162
+ if (!mergeTarget && collapseCount <= 1) {
163
+ scanFrom += Math.max(collapseCount, 1);
164
+ continue;
165
+ }
166
+
167
+ const collapsed = children.slice(scanFrom, collapseEndExclusive);
168
+ const startTime = getChatChildTime(collapsed[0]);
169
+ const endTime = getChatChildTime(collapsed[collapsed.length - 1]);
170
+ for (const child of collapsed) {
171
+ child.dispose?.();
172
+ }
173
+
174
+ if (mergeTarget) {
175
+ mergeTarget.merge(collapseCount, startTime, endTime);
176
+ children.splice(scanFrom, collapseCount);
177
+ chatChildAddedAt.set(mergeTarget, mergeTarget.startTime);
178
+ // scanFrom stays; whatever follows the merged run is next.
179
+ } else {
180
+ const placeholder = new CollapsedChatHistoryComponent(collapseCount, startTime, endTime);
181
+ children.splice(scanFrom, collapseCount, placeholder);
182
+ chatChildAddedAt.set(placeholder, startTime);
183
+ scanFrom++; // step past the freshly created placeholder
184
+ }
185
+ }
186
+ }
187
+
57
188
  export class UiHelpers {
58
189
  constructor(private ctx: InteractiveModeContext) {}
59
190
 
@@ -91,8 +222,8 @@ export class UiHelpers {
91
222
 
92
223
  const spacer = new Spacer(1);
93
224
  const text = new Text(rendered, 1, 0);
94
- this.ctx.chatContainer.addChild(spacer);
95
- this.ctx.chatContainer.addChild(text);
225
+ addChatChild(this.ctx, spacer);
226
+ addChatChild(this.ctx, text);
96
227
  this.ctx.lastStatusSpacer = spacer;
97
228
  this.ctx.lastStatusText = text;
98
229
  this.ctx.ui.requestRender();
@@ -108,7 +239,7 @@ export class UiHelpers {
108
239
  component.setComplete(message.exitCode, message.cancelled, {
109
240
  truncation: message.meta?.truncation,
110
241
  });
111
- this.ctx.chatContainer.addChild(component);
242
+ addChatChild(this.ctx, component);
112
243
  break;
113
244
  }
114
245
  case "pythonExecution": {
@@ -119,7 +250,7 @@ export class UiHelpers {
119
250
  component.setComplete(message.exitCode, message.cancelled, {
120
251
  truncation: message.meta?.truncation,
121
252
  });
122
- this.ctx.chatContainer.addChild(component);
253
+ addChatChild(this.ctx, component);
123
254
  break;
124
255
  }
125
256
  case "hookMessage":
@@ -163,14 +294,14 @@ export class UiHelpers {
163
294
  ]
164
295
  .filter(Boolean)
165
296
  .join(" ");
166
- this.ctx.chatContainer.addChild(new Text(line, 1, 0));
297
+ addChatChild(this.ctx, new Text(line, 1, 0));
167
298
  }
168
299
  break;
169
300
  }
170
301
  if (message.customType === SKILL_PROMPT_MESSAGE_TYPE) {
171
302
  const component = new SkillMessageComponent(message as CustomMessage<SkillPromptDetails>);
172
303
  component.setExpanded(this.ctx.toolOutputExpanded);
173
- this.ctx.chatContainer.addChild(component);
304
+ addChatChild(this.ctx, component);
174
305
  break;
175
306
  }
176
307
  if (
@@ -207,12 +338,12 @@ export class UiHelpers {
207
338
  const components: Component[] = [];
208
339
  const header = `${theme.fg("accent", `[IRC] ${arrow}`)}`;
209
340
  const headerComponent = new Text(header, 1, 0);
210
- this.ctx.chatContainer.addChild(headerComponent);
341
+ addChatChild(this.ctx, headerComponent);
211
342
  components.push(headerComponent);
212
343
  if (body) {
213
344
  for (const line of body.split("\n")) {
214
345
  const lineComponent = new Text(theme.fg("muted", ` ${line}`), 0, 0);
215
- this.ctx.chatContainer.addChild(lineComponent);
346
+ addChatChild(this.ctx, lineComponent);
216
347
  components.push(lineComponent);
217
348
  }
218
349
  }
@@ -230,12 +361,12 @@ export class UiHelpers {
230
361
  const components: Component[] = [];
231
362
  const header = `${theme.fg("accent", `[Steer ${details?.state ?? "queued"}] ${details?.from ?? "?"} ⇨ ${details?.to ?? "?"}`)}`;
232
363
  const headerComponent = new Text(header, 1, 0);
233
- this.ctx.chatContainer.addChild(headerComponent);
364
+ addChatChild(this.ctx, headerComponent);
234
365
  components.push(headerComponent);
235
366
  if (details?.body) {
236
367
  for (const line of details.body.split("\n")) {
237
368
  const lineComponent = new Text(theme.fg("muted", ` ${line}`), 0, 0);
238
- this.ctx.chatContainer.addChild(lineComponent);
369
+ addChatChild(this.ctx, lineComponent);
239
370
  components.push(lineComponent);
240
371
  }
241
372
  }
@@ -245,22 +376,22 @@ export class UiHelpers {
245
376
  // Both HookMessage and CustomMessage have the same structure, cast for compatibility
246
377
  const component = new CustomMessageComponent(message as CustomMessage<unknown>, renderer);
247
378
  component.setExpanded(this.ctx.toolOutputExpanded);
248
- this.ctx.chatContainer.addChild(component);
379
+ addChatChild(this.ctx, component);
249
380
  }
250
381
  break;
251
382
  }
252
383
  case "compactionSummary": {
253
- this.ctx.chatContainer.addChild(new Spacer(1));
384
+ addChatChild(this.ctx, new Spacer(1));
254
385
  const component = new CompactionSummaryMessageComponent(message);
255
386
  component.setExpanded(this.ctx.toolOutputExpanded);
256
- this.ctx.chatContainer.addChild(component);
387
+ addChatChild(this.ctx, component);
257
388
  break;
258
389
  }
259
390
  case "branchSummary": {
260
- this.ctx.chatContainer.addChild(new Spacer(1));
391
+ addChatChild(this.ctx, new Spacer(1));
261
392
  const component = new BranchSummaryMessageComponent(message);
262
393
  component.setExpanded(this.ctx.toolOutputExpanded);
263
- this.ctx.chatContainer.addChild(component);
394
+ addChatChild(this.ctx, component);
264
395
  break;
265
396
  }
266
397
  case "fileMention": {
@@ -281,7 +412,7 @@ export class UiHelpers {
281
412
  "accent",
282
413
  file.path,
283
414
  )} ${theme.fg("dim", suffix)}`;
284
- this.ctx.chatContainer.addChild(new Text(text, 0, 0));
415
+ addChatChild(this.ctx, new Text(text, 0, 0));
285
416
  }
286
417
  break;
287
418
  }
@@ -291,7 +422,7 @@ export class UiHelpers {
291
422
  if (textContent) {
292
423
  const isSynthetic = message.role === "developer" ? true : (message.synthetic ?? false);
293
424
  const userComponent = new UserMessageComponent(textContent, isSynthetic);
294
- this.ctx.chatContainer.addChild(userComponent);
425
+ addChatChild(this.ctx, userComponent);
295
426
  if (options?.populateHistory && message.role === "user" && !isSynthetic) {
296
427
  this.ctx.editor.addToHistory(textContent);
297
428
  }
@@ -302,7 +433,7 @@ export class UiHelpers {
302
433
  const assistantComponent = new AssistantMessageComponent(message, this.ctx.hideThinkingBlock, () =>
303
434
  this.ctx.ui.requestRender(),
304
435
  );
305
- this.ctx.chatContainer.addChild(assistantComponent);
436
+ addChatChild(this.ctx, assistantComponent);
306
437
  break;
307
438
  }
308
439
  case "toolResult": {
@@ -382,7 +513,7 @@ export class UiHelpers {
382
513
  showContentPreview: this.ctx.settings.get("read.toolResultPreview"),
383
514
  });
384
515
  readGroup.setExpanded(this.ctx.toolOutputExpanded);
385
- this.ctx.chatContainer.addChild(readGroup);
516
+ addChatChild(this.ctx, readGroup);
386
517
  }
387
518
  readGroup.updateArgs(content.arguments, content.id);
388
519
  readGroup.updateResult(
@@ -424,7 +555,7 @@ export class UiHelpers {
424
555
  content.id,
425
556
  );
426
557
  component.setExpanded(this.ctx.toolOutputExpanded);
427
- this.ctx.chatContainer.addChild(component);
558
+ addChatChild(this.ctx, component);
428
559
 
429
560
  if (hasErrorStop && errorMessage) {
430
561
  component.updateResult(
@@ -462,7 +593,7 @@ export class UiHelpers {
462
593
  showContentPreview: this.ctx.settings.get("read.toolResultPreview"),
463
594
  });
464
595
  readGroup.setExpanded(this.ctx.toolOutputExpanded);
465
- this.ctx.chatContainer.addChild(readGroup);
596
+ addChatChild(this.ctx, readGroup);
466
597
  }
467
598
  const args = readToolCallArgs.get(message.toolCallId);
468
599
  if (args) {
@@ -529,7 +660,7 @@ export class UiHelpers {
529
660
  }
530
661
  if (preservedChatChildren && preservedChatChildren.length > 0) {
531
662
  for (const child of preservedChatChildren) {
532
- this.ctx.chatContainer.addChild(child);
663
+ addChatChild(this.ctx, child);
533
664
  }
534
665
  this.ctx.ui.requestRender();
535
666
  }
@@ -549,8 +680,8 @@ export class UiHelpers {
549
680
  process.stderr.write(`Error: ${errorMessage}\n`);
550
681
  return;
551
682
  }
552
- this.ctx.chatContainer.addChild(new Spacer(1));
553
- this.ctx.chatContainer.addChild(new Text(theme.fg("error", `Error: ${errorMessage}`), 1, 0));
683
+ addChatChild(this.ctx, new Spacer(1));
684
+ addChatChild(this.ctx, new Text(theme.fg("error", `Error: ${errorMessage}`), 1, 0));
554
685
  this.ctx.ui.requestRender();
555
686
  }
556
687
 
@@ -559,15 +690,16 @@ export class UiHelpers {
559
690
  process.stderr.write(`Warning: ${warningMessage}\n`);
560
691
  return;
561
692
  }
562
- this.ctx.chatContainer.addChild(new Spacer(1));
563
- this.ctx.chatContainer.addChild(new Text(theme.fg("warning", `Warning: ${warningMessage}`), 1, 0));
693
+ addChatChild(this.ctx, new Spacer(1));
694
+ addChatChild(this.ctx, new Text(theme.fg("warning", `Warning: ${warningMessage}`), 1, 0));
564
695
  this.ctx.ui.requestRender();
565
696
  }
566
697
 
567
698
  showNewVersionNotification(newVersion: string): void {
568
- this.ctx.chatContainer.addChild(new Spacer(1));
569
- this.ctx.chatContainer.addChild(new DynamicBorder(text => theme.fg("warning", text)));
570
- this.ctx.chatContainer.addChild(
699
+ addChatChild(this.ctx, new Spacer(1));
700
+ addChatChild(this.ctx, new DynamicBorder(text => theme.fg("warning", text)));
701
+ addChatChild(
702
+ this.ctx,
571
703
  new Text(
572
704
  theme.bold(theme.fg("warning", "Update Available")) +
573
705
  "\n" +
@@ -577,7 +709,7 @@ export class UiHelpers {
577
709
  0,
578
710
  ),
579
711
  );
580
- this.ctx.chatContainer.addChild(new DynamicBorder(text => theme.fg("warning", text)));
712
+ addChatChild(this.ctx, new DynamicBorder(text => theme.fg("warning", text)));
581
713
  this.ctx.ui.requestRender();
582
714
  }
583
715
 
@@ -619,7 +751,11 @@ export class UiHelpers {
619
751
  }
620
752
 
621
753
  queueCompactionMessage(text: string, mode: "steer" | "followUp"): void {
622
- this.ctx.compactionQueuedMessages.push({ text, mode } as CompactionQueuedMessage);
754
+ const entry: CompactionQueuedMessage = { text, mode };
755
+ if (mode === "followUp") {
756
+ entry.followUpQueuePolicy = "sequential";
757
+ }
758
+ this.ctx.compactionQueuedMessages.push(entry);
623
759
  this.ctx.editor.addToHistory(text);
624
760
  this.ctx.editor.setText("");
625
761
  this.ctx.updatePendingMessagesDisplay();
@@ -633,6 +769,11 @@ export class UiHelpers {
633
769
  return this.#hasSkillInvocations(text) || this.isKnownSlashCommand(text);
634
770
  }
635
771
 
772
+ #compactionFollowUpQueuePolicy(message: CompactionQueuedMessage): "sequential" | undefined {
773
+ if (message.mode !== "followUp") return undefined;
774
+ return message.followUpQueuePolicy ?? "sequential";
775
+ }
776
+
636
777
  async #deliverQueuedSkillMessage(message: CompactionQueuedMessage): Promise<boolean> {
637
778
  const invocations = parseSkillInvocations(message.text, this.ctx.skillCommands ?? new Map());
638
779
  if (invocations.length === 0) {
@@ -675,6 +816,13 @@ export class UiHelpers {
675
816
  continue;
676
817
  }
677
818
 
819
+ const promptOptions =
820
+ message.mode === "followUp"
821
+ ? {
822
+ streamingBehavior: message.mode,
823
+ followUpQueuePolicy: this.#compactionFollowUpQueuePolicy(message),
824
+ }
825
+ : { streamingBehavior: message.mode };
678
826
  await this.ctx.session.promptCustomMessage(
679
827
  {
680
828
  customType: SKILL_PROMPT_MESSAGE_TYPE,
@@ -683,7 +831,7 @@ export class UiHelpers {
683
831
  details,
684
832
  attribution: "user",
685
833
  },
686
- { streamingBehavior: message.mode },
834
+ promptOptions,
687
835
  );
688
836
  }
689
837
 
@@ -703,7 +851,11 @@ export class UiHelpers {
703
851
  return;
704
852
  }
705
853
  await this.ctx.withLocalSubmission(message.text, () =>
706
- message.mode === "followUp" ? this.ctx.session.followUp(message.text) : this.ctx.session.steer(message.text),
854
+ message.mode === "followUp"
855
+ ? this.ctx.session.followUp(message.text, undefined, {
856
+ followUpQueuePolicy: this.#compactionFollowUpQueuePolicy(message),
857
+ })
858
+ : this.ctx.session.steer(message.text),
707
859
  );
708
860
  }
709
861
 
@@ -794,14 +946,17 @@ export class UiHelpers {
794
946
  // `restoreQueue` rather than rethrown, so we use the primitive
795
947
  // recordLocalSubmission and dispose manually in the catch.
796
948
  const disposeFirstPrompt = this.ctx.recordLocalSubmission(firstPrompt.text);
797
- const promptPromise = this.ctx.session
798
- .prompt(firstPrompt.text, {
799
- streamingBehavior: firstPrompt.mode === "followUp" ? "followUp" : "steer",
800
- })
801
- .catch((error: unknown) => {
802
- disposeFirstPrompt();
803
- restoreQueue(error);
804
- });
949
+ const firstPromptOptions =
950
+ firstPrompt.mode === "followUp"
951
+ ? {
952
+ streamingBehavior: "followUp" as const,
953
+ followUpQueuePolicy: this.#compactionFollowUpQueuePolicy(firstPrompt),
954
+ }
955
+ : { streamingBehavior: "steer" as const };
956
+ const promptPromise = this.ctx.session.prompt(firstPrompt.text, firstPromptOptions).catch((error: unknown) => {
957
+ disposeFirstPrompt();
958
+ restoreQueue(error);
959
+ });
805
960
 
806
961
  for (const message of rest) {
807
962
  await this.#deliverQueuedMessage(message);
@@ -820,12 +975,12 @@ export class UiHelpers {
820
975
  // removeChild() would tear them down before re-adding.
821
976
  for (const component of this.ctx.pendingBashComponents) {
822
977
  this.ctx.pendingMessagesContainer.detachChild(component);
823
- this.ctx.chatContainer.addChild(component);
978
+ addChatChild(this.ctx, component);
824
979
  }
825
980
  this.ctx.pendingBashComponents = [];
826
981
  for (const component of this.ctx.pendingPythonComponents) {
827
982
  this.ctx.pendingMessagesContainer.detachChild(component);
828
- this.ctx.chatContainer.addChild(component);
983
+ addChatChild(this.ctx, component);
829
984
  }
830
985
  this.ctx.pendingPythonComponents = [];
831
986
  }
@@ -20,6 +20,76 @@ export interface ConfigCommandChange {
20
20
  redact?: boolean;
21
21
  }
22
22
 
23
+ export type TelegramControlCommandName = "reasoning" | "usage" | "context" | "compact";
24
+
25
+ export type TelegramControlCommand =
26
+ | { name: "reasoning"; action: "cycle" | "status" | "set"; level?: string }
27
+ | { name: "usage" }
28
+ | { name: "context" }
29
+ | { name: "compact"; instructions?: string };
30
+
31
+ export type TelegramControlCommandParseResult =
32
+ | { kind: "none" }
33
+ | { kind: "ignored"; commandName: TelegramControlCommandName }
34
+ | { kind: "command"; command: TelegramControlCommand }
35
+ | { kind: "invalid"; commandName: TelegramControlCommandName; usage: string };
36
+
37
+ const TELEGRAM_CONTROL_COMMANDS = new Set<TelegramControlCommandName>(["reasoning", "usage", "context", "compact"]);
38
+ const TELEGRAM_REASONING_LEVELS = new Set(["inherit", "off", "minimal", "low", "medium", "high", "xhigh", "max"]);
39
+
40
+ function splitTelegramBotSuffix(rawCommand: string): { name: string; suffix?: string } {
41
+ const [name, suffix] = rawCommand.toLowerCase().split("@", 2);
42
+ return suffix ? { name, suffix } : { name };
43
+ }
44
+
45
+ export function telegramControlCommandUsage(commandName: TelegramControlCommandName): string {
46
+ switch (commandName) {
47
+ case "reasoning":
48
+ return "Usage: /reasoning [cycle|inherit|off|minimal|low|medium|high|xhigh|max]";
49
+ case "usage":
50
+ return "Usage: /usage";
51
+ case "context":
52
+ return "Usage: /context";
53
+ case "compact":
54
+ return "Usage: /compact [instructions]";
55
+ }
56
+ }
57
+
58
+ /** Parse deterministic Telegram session-control commands. Recognised roots fail closed. */
59
+ export function parseTelegramControlCommand(text: string, botUsername?: string): TelegramControlCommandParseResult {
60
+ const trimmed = text.trim();
61
+ if (!trimmed.startsWith("/")) return { kind: "none" };
62
+ const [rawRoot, ...rest] = trimmed.slice(1).split(/\s+/);
63
+ if (!rawRoot) return { kind: "none" };
64
+ const { name: root, suffix } = splitTelegramBotSuffix(rawRoot);
65
+ if (!TELEGRAM_CONTROL_COMMANDS.has(root as TelegramControlCommandName)) return { kind: "none" };
66
+ const commandName = root as TelegramControlCommandName;
67
+ if (suffix && (!botUsername || suffix !== botUsername.toLowerCase())) return { kind: "ignored", commandName };
68
+ const usage = telegramControlCommandUsage(commandName);
69
+
70
+ switch (commandName) {
71
+ case "usage":
72
+ case "context":
73
+ return rest.length === 0
74
+ ? { kind: "command", command: { name: commandName } }
75
+ : { kind: "invalid", commandName, usage };
76
+ case "compact": {
77
+ const instructions = trimmed.slice(rawRoot.length + 1).trim();
78
+ return { kind: "command", command: instructions ? { name: "compact", instructions } : { name: "compact" } };
79
+ }
80
+ case "reasoning": {
81
+ if (rest.length === 0) return { kind: "command", command: { name: "reasoning", action: "status" } };
82
+ if (rest.length !== 1) return { kind: "invalid", commandName, usage };
83
+ const levelOrAction = rest[0]!.toLowerCase();
84
+ if (levelOrAction === "cycle") return { kind: "command", command: { name: "reasoning", action: "cycle" } };
85
+ if (TELEGRAM_REASONING_LEVELS.has(levelOrAction)) {
86
+ return { kind: "command", command: { name: "reasoning", action: "set", level: levelOrAction } };
87
+ }
88
+ return { kind: "invalid", commandName, usage };
89
+ }
90
+ }
91
+ }
92
+
23
93
  /**
24
94
  * Parse an in-thread config command. Returns the requested change, or
25
95
  * `undefined` when the text is not a recognised config command (so the daemon