@gajae-code/coding-agent 0.9.2 → 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 (69) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/types/capability/mcp.d.ts +3 -1
  3. package/dist/types/commands/mcp.d.ts +1 -1
  4. package/dist/types/modes/components/status-line/types.d.ts +6 -0
  5. package/dist/types/modes/components/status-line.d.ts +1 -0
  6. package/dist/types/modes/utils/ui-helpers.d.ts +12 -0
  7. package/dist/types/runtime-mcp/config-writer.d.ts +4 -3
  8. package/dist/types/runtime-mcp/types.d.ts +5 -4
  9. package/dist/types/session/agent-session.d.ts +16 -0
  10. package/dist/types/session/session-manager.d.ts +1 -0
  11. package/dist/types/tools/skill.d.ts +9 -8
  12. package/dist/types/utils/linkedom.d.ts +4 -0
  13. package/dist/types/utils/pasted-image-path.d.ts +5 -3
  14. package/package.json +11 -7
  15. package/scripts/build-binary.ts +2 -40
  16. package/scripts/compile-args.ts +91 -0
  17. package/scripts/generate-docs-index.ts +1 -0
  18. package/src/capability/mcp.ts +3 -1
  19. package/src/cli/mcp-cli.ts +108 -26
  20. package/src/cli.ts +21 -0
  21. package/src/commands/mcp.ts +8 -8
  22. package/src/commands/stats.ts +6 -2
  23. package/src/config/mcp-schema.json +21 -17
  24. package/src/coordinator-mcp/server.ts +7 -4
  25. package/src/discovery/builtin.ts +14 -0
  26. package/src/discovery/mcp-json.ts +14 -0
  27. package/src/gjc-runtime/state-runtime.ts +105 -12
  28. package/src/gjc-runtime/workflow-manifest.generated.json +4 -28
  29. package/src/gjc-runtime/workflow-manifest.ts +1 -7
  30. package/src/internal-urls/docs-index.generated.ts +3 -2
  31. package/src/internal-urls/gjc-protocol.ts +14 -1
  32. package/src/modes/components/assistant-message.ts +1 -1
  33. package/src/modes/components/status-line/presets.ts +5 -7
  34. package/src/modes/components/status-line/segments.ts +14 -0
  35. package/src/modes/components/status-line/types.ts +6 -0
  36. package/src/modes/components/status-line.ts +7 -1
  37. package/src/modes/components/tool-execution.ts +1 -1
  38. package/src/modes/controllers/event-controller.ts +9 -9
  39. package/src/modes/controllers/input-controller.ts +2 -2
  40. package/src/modes/interactive-mode.ts +12 -4
  41. package/src/modes/utils/ui-helpers.ts +154 -32
  42. package/src/prompts/tools/skill.md +2 -2
  43. package/src/runtime-mcp/config-writer.ts +4 -3
  44. package/src/runtime-mcp/config.ts +1 -0
  45. package/src/runtime-mcp/types.ts +5 -4
  46. package/src/session/agent-session.ts +117 -3
  47. package/src/session/session-manager.ts +68 -40
  48. package/src/tools/bash-interactive.ts +9 -4
  49. package/src/tools/browser/readable.ts +2 -2
  50. package/src/tools/fetch.ts +5 -5
  51. package/src/tools/skill.ts +14 -11
  52. package/src/utils/linkedom.ts +14 -0
  53. package/src/utils/pasted-image-path.ts +23 -9
  54. package/src/web/scrapers/arxiv.ts +2 -2
  55. package/src/web/scrapers/go-pkg.ts +2 -2
  56. package/src/web/scrapers/iacr.ts +2 -2
  57. package/src/web/scrapers/readthedocs.ts +2 -2
  58. package/src/web/scrapers/twitter.ts +3 -2
  59. package/src/web/scrapers/wikipedia.ts +2 -2
  60. package/src/export/html/template.css +0 -1060
  61. package/src/export/html/template.html +0 -47
  62. package/src/export/html/template.js +0 -2314
  63. package/vendor/insane-search/engine/tests/test_hardening.py +0 -57
  64. package/vendor/insane-search/engine/tests/test_smoke.py +0 -152
  65. package/vendor/insane-search/engine/tests/test_u1.py +0 -200
  66. package/vendor/insane-search/engine/tests/test_u4.py +0 -131
  67. package/vendor/insane-search/engine/tests/test_u5.py +0 -163
  68. package/vendor/insane-search/engine/tests/test_u7.py +0 -124
  69. package/vendor/insane-search/engine/tests/test_u8.py +0 -216
@@ -26,7 +26,7 @@ import { interruptHint } from "../shared";
26
26
  import { buildAbortDisplayMessage } from "../utils/abort-message";
27
27
  import { consumeInjectedOptimisticSignature } from "../utils/injected-user-submission";
28
28
  import { ringTerminalBell } from "../utils/terminal-bell";
29
- import { argsWithPartialJson } from "../utils/ui-helpers";
29
+ import { addChatChild, argsWithPartialJson } from "../utils/ui-helpers";
30
30
 
31
31
  type AgentSessionEventKind = AgentSessionEvent["type"];
32
32
 
@@ -169,12 +169,12 @@ export class EventController {
169
169
 
170
170
  #getReadGroup(): ReadToolGroupComponent {
171
171
  if (!this.#lastReadGroup) {
172
- this.ctx.chatContainer.addChild(new Text("", 0, 0));
172
+ addChatChild(this.ctx, new Text("", 0, 0));
173
173
  const group = new ReadToolGroupComponent({
174
174
  showContentPreview: this.ctx.settings.get("read.toolResultPreview"),
175
175
  });
176
176
  group.setExpanded(this.ctx.toolOutputExpanded);
177
- this.ctx.chatContainer.addChild(group);
177
+ addChatChild(this.ctx, group);
178
178
  this.#lastReadGroup = group;
179
179
  }
180
180
  return this.#lastReadGroup;
@@ -332,7 +332,7 @@ export class EventController {
332
332
  this.ctx.ui.requestRender(),
333
333
  );
334
334
  this.ctx.streamingMessage = event.message;
335
- this.ctx.chatContainer.addChild(this.ctx.streamingComponent);
335
+ addChatChild(this.ctx, this.ctx.streamingComponent);
336
336
  this.ctx.streamingComponent.updateContent(this.ctx.streamingMessage, { streaming: true });
337
337
  this.ctx.ui.requestRender();
338
338
  }
@@ -445,7 +445,7 @@ export class EventController {
445
445
  );
446
446
  if (!this.ctx.pendingTools.has(content.id)) {
447
447
  this.#resetReadGroup();
448
- this.ctx.chatContainer.addChild(new Text("", 0, 0));
448
+ addChatChild(this.ctx, new Text("", 0, 0));
449
449
  const tool = this.ctx.session.getToolByName(content.name);
450
450
  const component = new ToolExecutionComponent(
451
451
  content.name,
@@ -462,8 +462,8 @@ export class EventController {
462
462
  content.id,
463
463
  );
464
464
  component.setExpanded(this.ctx.toolOutputExpanded);
465
- this.ctx.chatContainer.addChild(component);
466
465
  this.ctx.pendingTools.set(content.id, component);
466
+ addChatChild(this.ctx, component);
467
467
  } else {
468
468
  this.ctx.pendingTools.get(content.id)?.updateArgs(renderArgs, content.id);
469
469
  }
@@ -575,8 +575,8 @@ export class EventController {
575
575
  event.toolCallId,
576
576
  );
577
577
  component.setExpanded(this.ctx.toolOutputExpanded);
578
- this.ctx.chatContainer.addChild(component);
579
578
  this.ctx.pendingTools.set(event.toolCallId, component);
579
+ addChatChild(this.ctx, component);
580
580
  this.ctx.ui.requestRender();
581
581
  }
582
582
  }
@@ -869,13 +869,13 @@ export class EventController {
869
869
  async #handleTtsrTriggered(event: Extract<AgentSessionEvent, { type: "ttsr_triggered" }>): Promise<void> {
870
870
  const component = new TtsrNotificationComponent(event.rules);
871
871
  component.setExpanded(this.ctx.toolOutputExpanded);
872
- this.ctx.chatContainer.addChild(component);
872
+ addChatChild(this.ctx, component);
873
873
  this.ctx.ui.requestRender();
874
874
  }
875
875
 
876
876
  async #handleTodoReminder(event: Extract<AgentSessionEvent, { type: "todo_reminder" }>): Promise<void> {
877
877
  const component = new TodoReminderComponent(event.todos, event.attempt, event.maxAttempts);
878
- this.ctx.chatContainer.addChild(component);
878
+ addChatChild(this.ctx, component);
879
879
  this.ctx.ui.requestRender();
880
880
  }
881
881
 
@@ -18,7 +18,7 @@ import { copyToClipboard, readImageFromClipboard } from "../../utils/clipboard";
18
18
  import { getEditorCommand, openInEditor } from "../../utils/external-editor";
19
19
  import { ensureSupportedImageInput, ImageInputTooLargeError, loadImageInput } from "../../utils/image-loading";
20
20
  import { resizeImage } from "../../utils/image-resize";
21
- import { resolvePastedImagePath } from "../../utils/pasted-image-path";
21
+ import { formatPastedImageReference, resolvePastedImagePath } from "../../utils/pasted-image-path";
22
22
  import { generateSessionTitle, setSessionTerminalTitle } from "../../utils/title-generator";
23
23
  import { type QueuedMessageMoveDirection, QueuedMessageSelectorComponent } from "../components/queued-message-selector";
24
24
 
@@ -1096,7 +1096,7 @@ export class InputController {
1096
1096
  data: image.data,
1097
1097
  mimeType: image.mimeType,
1098
1098
  });
1099
- this.ctx.editor.insertText(`${this.#nextImagePlaceholder()} `);
1099
+ this.ctx.editor.insertText(`${formatPastedImageReference(this.#nextImagePlaceholder(), image.resolvedPath)} `);
1100
1100
  this.ctx.showStatus(`Attached image: ${path.basename(image.resolvedPath)}`, { dim: true });
1101
1101
  this.ctx.ui.requestRender();
1102
1102
  return true;
@@ -18,6 +18,7 @@ import type { Component, EditorTheme, SlashCommand } from "@gajae-code/tui";
18
18
  import {
19
19
  Container,
20
20
  clearRenderCache,
21
+ getRenderCacheRetainedBytes,
21
22
  Loader,
22
23
  Markdown,
23
24
  ProcessTerminal,
@@ -114,7 +115,7 @@ import {
114
115
  theme,
115
116
  } from "./theme/theme";
116
117
  import type { CompactionQueuedMessage, InteractiveModeContext, SubmittedUserInput, TodoItem, TodoPhase } from "./types";
117
- import { UiHelpers } from "./utils/ui-helpers";
118
+ import { addChatChild, UiHelpers } from "./utils/ui-helpers";
118
119
 
119
120
  const INTERACTIVE_ABORT_CLEANUP_TIMEOUT_MS = 5_000;
120
121
  const COMPOSER_NEWLINE_HINT = process.platform === "win32" ? "Alt+Enter/Ctrl+J" : "Shift+Enter/Ctrl+J";
@@ -398,6 +399,10 @@ export class InteractiveMode implements InteractiveModeContext {
398
399
  ) {
399
400
  this.session = session;
400
401
  this.sessionManager = session.sessionManager;
402
+ this.session.setRetainedMemorySampler(() => ({
403
+ tuiChatChildren: this.chatContainer.children.length,
404
+ tuiCachedRenderBytes: getRenderCacheRetainedBytes(),
405
+ }));
401
406
  this.settings = session.settings;
402
407
  this.keybindings = KeybindingsManager.inMemory();
403
408
  this.agent = session.agent;
@@ -1514,8 +1519,11 @@ export class InteractiveMode implements InteractiveModeContext {
1514
1519
 
1515
1520
  #renderPlanPreview(planContent: string, options?: { append?: boolean }): void {
1516
1521
  const existingContainer = this.#planReviewContainer;
1517
- const replaceExisting = options?.append !== true && existingContainer !== undefined;
1518
- const planReviewContainer = replaceExisting ? existingContainer : new Container();
1522
+ // Only reuse the existing preview when it is still attached to the chat container;
1523
+ // a collapsed/disposed container must be re-added, not mutated in place.
1524
+ const stillAttached = existingContainer !== undefined && this.chatContainer.children.includes(existingContainer);
1525
+ const replaceExisting = options?.append !== true && stillAttached;
1526
+ const planReviewContainer = replaceExisting && existingContainer ? existingContainer : new Container();
1519
1527
  planReviewContainer.clear();
1520
1528
  planReviewContainer.addChild(new Spacer(1));
1521
1529
  planReviewContainer.addChild(new DynamicBorder());
@@ -1524,7 +1532,7 @@ export class InteractiveMode implements InteractiveModeContext {
1524
1532
  planReviewContainer.addChild(new Markdown(planContent, 1, 1, getMarkdownTheme()));
1525
1533
  planReviewContainer.addChild(new DynamicBorder());
1526
1534
  if (!replaceExisting) {
1527
- this.chatContainer.addChild(planReviewContainer);
1535
+ addChatChild(this, planReviewContainer);
1528
1536
  }
1529
1537
  this.#planReviewContainer = planReviewContainer;
1530
1538
  this.ui.requestRender();
@@ -64,6 +64,127 @@ type QueuedMessages = {
64
64
  followUp: string[];
65
65
  };
66
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
+
67
188
  export class UiHelpers {
68
189
  constructor(private ctx: InteractiveModeContext) {}
69
190
 
@@ -101,8 +222,8 @@ export class UiHelpers {
101
222
 
102
223
  const spacer = new Spacer(1);
103
224
  const text = new Text(rendered, 1, 0);
104
- this.ctx.chatContainer.addChild(spacer);
105
- this.ctx.chatContainer.addChild(text);
225
+ addChatChild(this.ctx, spacer);
226
+ addChatChild(this.ctx, text);
106
227
  this.ctx.lastStatusSpacer = spacer;
107
228
  this.ctx.lastStatusText = text;
108
229
  this.ctx.ui.requestRender();
@@ -118,7 +239,7 @@ export class UiHelpers {
118
239
  component.setComplete(message.exitCode, message.cancelled, {
119
240
  truncation: message.meta?.truncation,
120
241
  });
121
- this.ctx.chatContainer.addChild(component);
242
+ addChatChild(this.ctx, component);
122
243
  break;
123
244
  }
124
245
  case "pythonExecution": {
@@ -129,7 +250,7 @@ export class UiHelpers {
129
250
  component.setComplete(message.exitCode, message.cancelled, {
130
251
  truncation: message.meta?.truncation,
131
252
  });
132
- this.ctx.chatContainer.addChild(component);
253
+ addChatChild(this.ctx, component);
133
254
  break;
134
255
  }
135
256
  case "hookMessage":
@@ -173,14 +294,14 @@ export class UiHelpers {
173
294
  ]
174
295
  .filter(Boolean)
175
296
  .join(" ");
176
- this.ctx.chatContainer.addChild(new Text(line, 1, 0));
297
+ addChatChild(this.ctx, new Text(line, 1, 0));
177
298
  }
178
299
  break;
179
300
  }
180
301
  if (message.customType === SKILL_PROMPT_MESSAGE_TYPE) {
181
302
  const component = new SkillMessageComponent(message as CustomMessage<SkillPromptDetails>);
182
303
  component.setExpanded(this.ctx.toolOutputExpanded);
183
- this.ctx.chatContainer.addChild(component);
304
+ addChatChild(this.ctx, component);
184
305
  break;
185
306
  }
186
307
  if (
@@ -217,12 +338,12 @@ export class UiHelpers {
217
338
  const components: Component[] = [];
218
339
  const header = `${theme.fg("accent", `[IRC] ${arrow}`)}`;
219
340
  const headerComponent = new Text(header, 1, 0);
220
- this.ctx.chatContainer.addChild(headerComponent);
341
+ addChatChild(this.ctx, headerComponent);
221
342
  components.push(headerComponent);
222
343
  if (body) {
223
344
  for (const line of body.split("\n")) {
224
345
  const lineComponent = new Text(theme.fg("muted", ` ${line}`), 0, 0);
225
- this.ctx.chatContainer.addChild(lineComponent);
346
+ addChatChild(this.ctx, lineComponent);
226
347
  components.push(lineComponent);
227
348
  }
228
349
  }
@@ -240,12 +361,12 @@ export class UiHelpers {
240
361
  const components: Component[] = [];
241
362
  const header = `${theme.fg("accent", `[Steer ${details?.state ?? "queued"}] ${details?.from ?? "?"} ⇨ ${details?.to ?? "?"}`)}`;
242
363
  const headerComponent = new Text(header, 1, 0);
243
- this.ctx.chatContainer.addChild(headerComponent);
364
+ addChatChild(this.ctx, headerComponent);
244
365
  components.push(headerComponent);
245
366
  if (details?.body) {
246
367
  for (const line of details.body.split("\n")) {
247
368
  const lineComponent = new Text(theme.fg("muted", ` ${line}`), 0, 0);
248
- this.ctx.chatContainer.addChild(lineComponent);
369
+ addChatChild(this.ctx, lineComponent);
249
370
  components.push(lineComponent);
250
371
  }
251
372
  }
@@ -255,22 +376,22 @@ export class UiHelpers {
255
376
  // Both HookMessage and CustomMessage have the same structure, cast for compatibility
256
377
  const component = new CustomMessageComponent(message as CustomMessage<unknown>, renderer);
257
378
  component.setExpanded(this.ctx.toolOutputExpanded);
258
- this.ctx.chatContainer.addChild(component);
379
+ addChatChild(this.ctx, component);
259
380
  }
260
381
  break;
261
382
  }
262
383
  case "compactionSummary": {
263
- this.ctx.chatContainer.addChild(new Spacer(1));
384
+ addChatChild(this.ctx, new Spacer(1));
264
385
  const component = new CompactionSummaryMessageComponent(message);
265
386
  component.setExpanded(this.ctx.toolOutputExpanded);
266
- this.ctx.chatContainer.addChild(component);
387
+ addChatChild(this.ctx, component);
267
388
  break;
268
389
  }
269
390
  case "branchSummary": {
270
- this.ctx.chatContainer.addChild(new Spacer(1));
391
+ addChatChild(this.ctx, new Spacer(1));
271
392
  const component = new BranchSummaryMessageComponent(message);
272
393
  component.setExpanded(this.ctx.toolOutputExpanded);
273
- this.ctx.chatContainer.addChild(component);
394
+ addChatChild(this.ctx, component);
274
395
  break;
275
396
  }
276
397
  case "fileMention": {
@@ -291,7 +412,7 @@ export class UiHelpers {
291
412
  "accent",
292
413
  file.path,
293
414
  )} ${theme.fg("dim", suffix)}`;
294
- this.ctx.chatContainer.addChild(new Text(text, 0, 0));
415
+ addChatChild(this.ctx, new Text(text, 0, 0));
295
416
  }
296
417
  break;
297
418
  }
@@ -301,7 +422,7 @@ export class UiHelpers {
301
422
  if (textContent) {
302
423
  const isSynthetic = message.role === "developer" ? true : (message.synthetic ?? false);
303
424
  const userComponent = new UserMessageComponent(textContent, isSynthetic);
304
- this.ctx.chatContainer.addChild(userComponent);
425
+ addChatChild(this.ctx, userComponent);
305
426
  if (options?.populateHistory && message.role === "user" && !isSynthetic) {
306
427
  this.ctx.editor.addToHistory(textContent);
307
428
  }
@@ -312,7 +433,7 @@ export class UiHelpers {
312
433
  const assistantComponent = new AssistantMessageComponent(message, this.ctx.hideThinkingBlock, () =>
313
434
  this.ctx.ui.requestRender(),
314
435
  );
315
- this.ctx.chatContainer.addChild(assistantComponent);
436
+ addChatChild(this.ctx, assistantComponent);
316
437
  break;
317
438
  }
318
439
  case "toolResult": {
@@ -392,7 +513,7 @@ export class UiHelpers {
392
513
  showContentPreview: this.ctx.settings.get("read.toolResultPreview"),
393
514
  });
394
515
  readGroup.setExpanded(this.ctx.toolOutputExpanded);
395
- this.ctx.chatContainer.addChild(readGroup);
516
+ addChatChild(this.ctx, readGroup);
396
517
  }
397
518
  readGroup.updateArgs(content.arguments, content.id);
398
519
  readGroup.updateResult(
@@ -434,7 +555,7 @@ export class UiHelpers {
434
555
  content.id,
435
556
  );
436
557
  component.setExpanded(this.ctx.toolOutputExpanded);
437
- this.ctx.chatContainer.addChild(component);
558
+ addChatChild(this.ctx, component);
438
559
 
439
560
  if (hasErrorStop && errorMessage) {
440
561
  component.updateResult(
@@ -472,7 +593,7 @@ export class UiHelpers {
472
593
  showContentPreview: this.ctx.settings.get("read.toolResultPreview"),
473
594
  });
474
595
  readGroup.setExpanded(this.ctx.toolOutputExpanded);
475
- this.ctx.chatContainer.addChild(readGroup);
596
+ addChatChild(this.ctx, readGroup);
476
597
  }
477
598
  const args = readToolCallArgs.get(message.toolCallId);
478
599
  if (args) {
@@ -539,7 +660,7 @@ export class UiHelpers {
539
660
  }
540
661
  if (preservedChatChildren && preservedChatChildren.length > 0) {
541
662
  for (const child of preservedChatChildren) {
542
- this.ctx.chatContainer.addChild(child);
663
+ addChatChild(this.ctx, child);
543
664
  }
544
665
  this.ctx.ui.requestRender();
545
666
  }
@@ -559,8 +680,8 @@ export class UiHelpers {
559
680
  process.stderr.write(`Error: ${errorMessage}\n`);
560
681
  return;
561
682
  }
562
- this.ctx.chatContainer.addChild(new Spacer(1));
563
- 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));
564
685
  this.ctx.ui.requestRender();
565
686
  }
566
687
 
@@ -569,15 +690,16 @@ export class UiHelpers {
569
690
  process.stderr.write(`Warning: ${warningMessage}\n`);
570
691
  return;
571
692
  }
572
- this.ctx.chatContainer.addChild(new Spacer(1));
573
- 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));
574
695
  this.ctx.ui.requestRender();
575
696
  }
576
697
 
577
698
  showNewVersionNotification(newVersion: string): void {
578
- this.ctx.chatContainer.addChild(new Spacer(1));
579
- this.ctx.chatContainer.addChild(new DynamicBorder(text => theme.fg("warning", text)));
580
- 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,
581
703
  new Text(
582
704
  theme.bold(theme.fg("warning", "Update Available")) +
583
705
  "\n" +
@@ -587,7 +709,7 @@ export class UiHelpers {
587
709
  0,
588
710
  ),
589
711
  );
590
- this.ctx.chatContainer.addChild(new DynamicBorder(text => theme.fg("warning", text)));
712
+ addChatChild(this.ctx, new DynamicBorder(text => theme.fg("warning", text)));
591
713
  this.ctx.ui.requestRender();
592
714
  }
593
715
 
@@ -853,12 +975,12 @@ export class UiHelpers {
853
975
  // removeChild() would tear them down before re-adding.
854
976
  for (const component of this.ctx.pendingBashComponents) {
855
977
  this.ctx.pendingMessagesContainer.detachChild(component);
856
- this.ctx.chatContainer.addChild(component);
978
+ addChatChild(this.ctx, component);
857
979
  }
858
980
  this.ctx.pendingBashComponents = [];
859
981
  for (const component of this.ctx.pendingPythonComponents) {
860
982
  this.ctx.pendingMessagesContainer.detachChild(component);
861
- this.ctx.chatContainer.addChild(component);
983
+ addChatChild(this.ctx, component);
862
984
  }
863
985
  this.ctx.pendingPythonComponents = [];
864
986
  }
@@ -8,8 +8,8 @@ Invoke another available skill in the current turn.
8
8
  <instruction>
9
9
  - `name` is the skill name as it appears in `/skill:<name>` (e.g. `ralplan`, `ultragoal`, `team`, `deep-interview`)
10
10
  - `args` is the free-form argument string the skill would receive after `/skill:<name>` on the command line
11
- - The tool loads the callee's SKILL.md into the current turn and handles the caller→callee state handoff itself.
12
- - The chain is refused while the caller skill is still active. If your current skill has not yet reached a terminal phase, prepare it first with `gjc state <skill> write --input '{"current_phase":"handoff"}' --json`; no other handoff command is needed.
11
+ - The tool loads the callee's SKILL.md into the current turn and handles native workflow caller→callee state handoff when the caller is one of the built-in GJC workflows.
12
+ - The chain is refused while a native workflow caller is still active. If your current skill is one of `deep-interview`, `ralplan`, `ultragoal`, or `team` and has not yet reached a terminal phase, prepare it first with `gjc state <skill> write --input '{"current_phase":"handoff"}' --json`; no other handoff command is needed. Runtime project/user skills do not use `gjc state <skill>`.
13
13
  - Call once per chain step. To chain `A → B → C`, A calls `skill(B)`; B's next agent turn calls `skill(C)`.
14
14
  </instruction>
15
15
 
@@ -238,10 +238,11 @@ export async function listMCPServers(filePath: string): Promise<string[]> {
238
238
  }
239
239
 
240
240
  /**
241
- * Set a server's autoload flag (connect automatically at session startup).
241
+ * Set a server's autoload flag for explicit runtime MCP consumers.
242
242
  *
243
- * Autoload defaults to true, so turning it on removes the key to keep the
244
- * config file free of redundant fields; turning it off writes `autoload: false`.
243
+ * Autoload defaults to true for consumers that honor it, so turning it on
244
+ * removes the key to keep the config file free of redundant fields; turning it
245
+ * off writes `autoload: false`.
245
246
  *
246
247
  * @throws Error if the server doesn't exist
247
248
  */
@@ -56,6 +56,7 @@ function convertToLegacyConfig(server: MCPServer): MCPServerConfig {
56
56
  };
57
57
  if (server.args) config.args = server.args;
58
58
  if (server.env) config.env = server.env;
59
+ if (server.noInheritEnv !== undefined) config.noInheritEnv = server.noInheritEnv;
59
60
  if (server.cwd) config.cwd = server.cwd;
60
61
  return config;
61
62
  }
@@ -62,9 +62,10 @@ interface MCPServerConfigBase {
62
62
  /** Whether this server is enabled (default: true) */
63
63
  enabled?: boolean;
64
64
  /**
65
- * Whether to connect this server automatically at session startup (default: true).
66
- * When false the server stays configured-but-disconnected until connected
67
- * explicitly (e.g. `/mcp connect <name>`).
65
+ * Whether an explicit runtime MCP consumer should connect this server
66
+ * automatically when that consumer starts (default: true). Normal standalone
67
+ * `gjc`, `gjc --tmux`, and print-mode sessions do not consume `gjc mcp`
68
+ * registrations today.
68
69
  */
69
70
  autoload?: boolean;
70
71
  /** Connection timeout in milliseconds (default: 30000) */
@@ -114,7 +115,7 @@ export interface MCPSseServerConfig extends MCPServerConfigBase {
114
115
  export type MCPServerConfig = MCPStdioServerConfig | MCPHttpServerConfig | MCPSseServerConfig;
115
116
 
116
117
  export const MCP_CONFIG_SCHEMA_URL =
117
- "https://raw.githubusercontent.com/can1357/gajae-code/main/packages/coding-agent/src/config/mcp-schema.json";
118
+ "https://raw.githubusercontent.com/Yeachan-Heo/gajae-code/main/packages/coding-agent/src/config/mcp-schema.json";
118
119
 
119
120
  /** Root mcp.json/.mcp.json file structure */
120
121
  export interface MCPConfigFile {