@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
@@ -269,7 +269,7 @@ export class AssistantMessageComponent extends Container {
269
269
  displayImage.data,
270
270
  displayImage.mimeType,
271
271
  { fallbackColor: (text: string) => theme.fg("toolOutput", text) },
272
- resolveImageOptions(),
272
+ { ...resolveImageOptions(), refetch: () => displayImage.data },
273
273
  ),
274
274
  ),
275
275
  });
@@ -1,4 +1,4 @@
1
- import type { ThinkingLevel } from "@gajae-code/agent-core";
1
+ import { ThinkingLevel, type ThinkingLevel as ThinkingLevelValue } from "@gajae-code/agent-core";
2
2
  import type { Effort } from "@gajae-code/ai";
3
3
  import {
4
4
  Container,
@@ -654,7 +654,7 @@ export interface SettingsRuntimeContext {
654
654
  /** Available thinking levels (from session) */
655
655
  availableThinkingLevels: Effort[];
656
656
  /** Current thinking level (from session) */
657
- thinkingLevel: ThinkingLevel | undefined;
657
+ thinkingLevel: ThinkingLevelValue | undefined;
658
658
  /** Available themes */
659
659
  availableThemes: string[];
660
660
  /** Available model profile names (from the model registry) */
@@ -843,7 +843,7 @@ export class SettingsSelectorComponent extends Container {
843
843
 
844
844
  // Special case: inject runtime options for thinking level
845
845
  if (def.path === "defaultThinkingLevel") {
846
- options = this.context.availableThinkingLevels.map(level => {
846
+ options = [ThinkingLevel.Off, ...this.context.availableThinkingLevels].map(level => {
847
847
  const baseOpt = options.find(o => o.value === level);
848
848
  return baseOpt || { value: level, label: level };
849
849
  });
@@ -3,7 +3,7 @@ import type { PresetDef, StatusLinePreset } from "./types";
3
3
  export const STATUS_LINE_PRESETS: Record<StatusLinePreset, PresetDef> = {
4
4
  default: {
5
5
  leftSegments: ["model", "mode", "git", "pr", "path"],
6
- rightSegments: ["session_name", "jobs", "token_rate", "context_pct", "cost"],
6
+ rightSegments: ["session_name", "jobs", "token_rate", "cost"],
7
7
  separator: "slash",
8
8
  segmentOptions: {
9
9
  model: { showThinkingLevel: true },
@@ -14,7 +14,7 @@ export const STATUS_LINE_PRESETS: Record<StatusLinePreset, PresetDef> = {
14
14
 
15
15
  "default-usage": {
16
16
  leftSegments: ["model", "mode", "git", "pr", "path"],
17
- rightSegments: ["session_name", "jobs", "token_rate", "usage", "context_pct", "cost"],
17
+ rightSegments: ["session_name", "jobs", "token_rate", "usage", "cost"],
18
18
  separator: "slash",
19
19
  segmentOptions: {
20
20
  model: { showThinkingLevel: true },
@@ -35,7 +35,7 @@ export const STATUS_LINE_PRESETS: Record<StatusLinePreset, PresetDef> = {
35
35
 
36
36
  compact: {
37
37
  leftSegments: ["model", "mode", "git", "pr"],
38
- rightSegments: ["session_name", "jobs", "cost", "context_pct"],
38
+ rightSegments: ["session_name", "jobs", "cost"],
39
39
  separator: "slash",
40
40
  segmentOptions: {
41
41
  model: { showThinkingLevel: false },
@@ -53,7 +53,6 @@ export const STATUS_LINE_PRESETS: Record<StatusLinePreset, PresetDef> = {
53
53
  "token_rate",
54
54
  "cache_read",
55
55
  "cost",
56
- "context_pct",
57
56
  "time_spent",
58
57
  "time",
59
58
  ],
@@ -78,7 +77,6 @@ export const STATUS_LINE_PRESETS: Record<StatusLinePreset, PresetDef> = {
78
77
  "cache_write",
79
78
  "token_rate",
80
79
  "cost",
81
- "context_pct",
82
80
  "context_total",
83
81
  "time_spent",
84
82
  "time",
@@ -95,7 +93,7 @@ export const STATUS_LINE_PRESETS: Record<StatusLinePreset, PresetDef> = {
95
93
  ascii: {
96
94
  // No Nerd Font dependencies
97
95
  leftSegments: ["model", "mode", "path", "git", "pr"],
98
- rightSegments: ["session_name", "jobs", "token_total", "cost", "context_pct"],
96
+ rightSegments: ["session_name", "jobs", "token_total", "cost"],
99
97
  separator: "ascii",
100
98
  segmentOptions: {
101
99
  model: { showThinkingLevel: true },
@@ -107,7 +105,7 @@ export const STATUS_LINE_PRESETS: Record<StatusLinePreset, PresetDef> = {
107
105
  custom: {
108
106
  // User-defined - these are just defaults that get overridden
109
107
  leftSegments: ["model", "mode", "path", "git", "pr"],
110
- rightSegments: ["session_name", "jobs", "token_total", "cost", "context_pct"],
108
+ rightSegments: ["session_name", "jobs", "token_total", "cost"],
111
109
  separator: "slash",
112
110
  segmentOptions: {},
113
111
  },
@@ -105,6 +105,20 @@ const modelSegment: StatusLineSegment = {
105
105
  }
106
106
  }
107
107
  }
108
+ // Show the context-window token percentage right next to the model /
109
+ // reasoning effort (rather than as a trailing segment) so it stays
110
+ // grouped with the model it describes. Disable per-preset with
111
+ // `segmentOptions.model.showContextPercent: false`. Suppressed
112
+ // automatically when a standalone context_pct segment is also in the
113
+ // active layout, so the value is never shown twice.
114
+ if (opts.showContextPercent !== false && ctx.contextPctSegmentActive !== true) {
115
+ const pct = ctx.contextPercent;
116
+ const window = ctx.contextWindow;
117
+ if (window > 0 && Number.isFinite(pct)) {
118
+ const color = getContextUsageThemeColor(getContextUsageLevel(pct, window));
119
+ content += `${theme.sep.dot}${theme.fg(color, `${pct.toFixed(1)}%`)}`;
120
+ }
121
+ }
108
122
 
109
123
  return { content: theme.fg("statusLineModel", content), visible: true };
110
124
  },
@@ -41,6 +41,12 @@ export interface SegmentContext {
41
41
  };
42
42
  contextPercent: number;
43
43
  contextWindow: number;
44
+ /**
45
+ * True when a standalone `context_pct` segment is also part of the active
46
+ * layout. The model segment suppresses its inline percentage in that case to
47
+ * avoid showing the same value twice.
48
+ */
49
+ contextPctSegmentActive?: boolean;
44
50
  autoCompactEnabled: boolean;
45
51
  subagentCount: number;
46
52
  jobs: JobsSnapshot;
@@ -29,7 +29,7 @@ import { calculateTokensPerSecond } from "./status-line/token-rate";
29
29
  import type { SeparatorDef } from "./status-line/types";
30
30
 
31
31
  export interface StatusLineSegmentOptions {
32
- model?: { showThinkingLevel?: boolean };
32
+ model?: { showThinkingLevel?: boolean; showContextPercent?: boolean };
33
33
  path?: { abbreviate?: boolean; maxLength?: number; stripWorkPrefix?: boolean };
34
34
  git?: { showBranch?: boolean; showStaged?: boolean; showUnstaged?: boolean; showUntracked?: boolean };
35
35
  time?: { format?: "12h" | "24h"; showSeconds?: boolean };
@@ -700,6 +700,11 @@ export class StatusLineComponent implements Component {
700
700
  const contextTokens = breakdown.usedTokens;
701
701
  const contextWindow = breakdown.contextWindow || state.model?.contextWindow || 0;
702
702
  const contextPercent = contextWindow > 0 ? (contextTokens / contextWindow) * 100 : 0;
703
+ // Suppress the inline model percentage when a standalone context_pct
704
+ // segment is also rendered, so the value is not shown twice.
705
+ const contextPctSegmentActive =
706
+ effectiveSettings.leftSegments.includes("context_pct") ||
707
+ effectiveSettings.rightSegments.includes("context_pct");
703
708
 
704
709
  return {
705
710
  session: this.session,
@@ -710,6 +715,7 @@ export class StatusLineComponent implements Component {
710
715
  usageStats,
711
716
  contextPercent,
712
717
  contextWindow,
718
+ contextPctSegmentActive,
713
719
  autoCompactEnabled: this.#autoCompactEnabled,
714
720
  subagentCount: this.#subagentCount,
715
721
  jobs: this.#jobs,
@@ -1,53 +1,114 @@
1
1
  import { ThinkingLevel, type ThinkingLevel as ThinkingLevelValue } from "@gajae-code/agent-core";
2
- import { Container, type SelectItem, SelectList } from "@gajae-code/tui";
3
- import { getSelectListTheme } from "../../modes/theme/theme";
2
+ import { Container, type SelectItem, SelectList, Text } from "@gajae-code/tui";
3
+ import { getSelectListTheme, theme } from "../../modes/theme/theme";
4
4
  import { getThinkingLevelMetadata, type ThinkingLevelValue as ThinkingMetadataValue } from "../../thinking-metadata";
5
5
  import { DynamicBorder } from "./dynamic-border";
6
6
 
7
+ const SCOPE_ITEMS = [
8
+ {
9
+ value: "session",
10
+ label: "Apply for this session",
11
+ description: "Use this effort until the session changes it again",
12
+ },
13
+ {
14
+ value: "default",
15
+ label: "Set as default",
16
+ description: "Save this effort for future sessions",
17
+ },
18
+ ] as const satisfies readonly SelectItem[];
19
+
20
+ export interface ThinkingSelectorSelection {
21
+ level: ThinkingLevelValue;
22
+ persistDefault: boolean;
23
+ }
24
+
7
25
  /**
8
26
  * Component that renders a thinking level selector with borders
9
27
  */
10
28
  export class ThinkingSelectorComponent extends Container {
11
29
  #selectList: SelectList;
30
+ #levelList: SelectList;
31
+ #selectedLevel: ThinkingLevelValue | undefined;
32
+ readonly #onSelect: (selection: ThinkingSelectorSelection) => void;
33
+ readonly #onCancel: () => void;
12
34
 
13
35
  constructor(
14
36
  currentLevel: ThinkingLevelValue | undefined,
15
37
  availableLevels: ThinkingLevelValue[],
16
- onSelect: (level: ThinkingLevelValue) => void,
38
+ onSelect: (selection: ThinkingSelectorSelection) => void,
17
39
  onCancel: () => void,
18
40
  ) {
19
41
  super();
20
42
 
21
- const thinkingLevels: SelectItem[] = availableLevels.map(level =>
22
- getThinkingLevelMetadata(level as ThinkingMetadataValue),
23
- );
43
+ this.#onSelect = onSelect;
44
+ this.#onCancel = onCancel;
24
45
 
25
- // Add top border
26
- this.addChild(new DynamicBorder());
46
+ const currentValue = currentLevel ?? ThinkingLevel.Off;
47
+ const thinkingLevels: SelectItem[] = availableLevels.map(level => {
48
+ const metadata = getThinkingLevelMetadata(level as ThinkingMetadataValue);
49
+ return level === currentValue
50
+ ? { ...metadata, label: metadata.label + theme.fg("muted", " (current)") }
51
+ : metadata;
52
+ });
27
53
 
28
- // Create selector
29
- this.#selectList = new SelectList(thinkingLevels, thinkingLevels.length, getSelectListTheme());
54
+ this.#levelList = new SelectList(thinkingLevels, thinkingLevels.length, getSelectListTheme());
55
+ this.#selectList = this.#levelList;
30
56
 
31
57
  // Preselect current level
32
- const currentIndex = thinkingLevels.findIndex(item => item.value === (currentLevel ?? ThinkingLevel.Off));
58
+ const currentIndex = thinkingLevels.findIndex(item => item.value === currentValue);
33
59
  if (currentIndex !== -1) {
34
- this.#selectList.setSelectedIndex(currentIndex);
60
+ this.#levelList.setSelectedIndex(currentIndex);
35
61
  }
36
62
 
37
- this.#selectList.onSelect = item => {
38
- onSelect(item.value as ThinkingLevelValue);
63
+ this.#levelList.onSelect = item => {
64
+ this.#selectedLevel = item.value as ThinkingLevelValue;
65
+ this.#renderScopeList();
39
66
  };
40
67
 
41
- this.#selectList.onCancel = () => {
42
- onCancel();
68
+ this.#levelList.onCancel = () => {
69
+ this.#onCancel();
43
70
  };
44
71
 
45
- this.addChild(this.#selectList);
72
+ this.#renderLevelList();
73
+ }
74
+
75
+ #renderLevelList(): void {
76
+ this.detachAll();
77
+ this.#selectedLevel = undefined;
78
+ this.#selectList = this.#levelList;
79
+ this.addChild(new DynamicBorder());
80
+ this.addChild(this.#levelList);
81
+ this.addChild(new DynamicBorder());
82
+ }
83
+
84
+ #renderScopeList(): void {
85
+ const level = this.#selectedLevel;
86
+ if (!level) return;
87
+
88
+ const scopeList = new SelectList(SCOPE_ITEMS, SCOPE_ITEMS.length, getSelectListTheme());
89
+ scopeList.onSelect = item => {
90
+ this.#onSelect({
91
+ level,
92
+ persistDefault: item.value === "default",
93
+ });
94
+ };
95
+ scopeList.onCancel = () => {
96
+ this.#renderLevelList();
97
+ };
46
98
 
47
- // Add bottom border
99
+ const metadata = getThinkingLevelMetadata(level as ThinkingMetadataValue);
100
+ this.detachAll();
101
+ this.#selectList = scopeList;
102
+ this.addChild(new DynamicBorder());
103
+ this.addChild(new Text(theme.fg("muted", ` Reasoning effort: ${metadata.label}`), 0, 0));
104
+ this.addChild(scopeList);
48
105
  this.addChild(new DynamicBorder());
49
106
  }
50
107
 
108
+ handleInput(data: string): void {
109
+ this.#selectList.handleInput(data);
110
+ }
111
+
51
112
  getSelectList(): SelectList {
52
113
  return this.#selectList;
53
114
  }
@@ -678,7 +678,7 @@ export class ToolExecutionComponent extends Container {
678
678
  imageData,
679
679
  imageMimeType,
680
680
  { fallbackColor: (s: string) => theme.fg("toolOutput", s) },
681
- resolveImageOptions(),
681
+ { ...resolveImageOptions(), refetch: () => imageData },
682
682
  );
683
683
  this.#imageComponents.push(imageComponent);
684
684
  this.addChild(imageComponent);
@@ -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
 
@@ -27,6 +27,8 @@ interface Expandable {
27
27
  }
28
28
 
29
29
  const INTERACTIVE_ABORT_CLEANUP_TIMEOUT_MS = 5_000;
30
+ const IMAGE_PLACEHOLDER_PATTERN = /\[image ([1-9]\d*)\]/g;
31
+ const IMAGE_PLACEHOLDER_PRESENT_PATTERN = /\[image [1-9]\d*\]/;
30
32
 
31
33
  function isExpandable(obj: unknown): obj is Expandable {
32
34
  return typeof obj === "object" && obj !== null && "setExpanded" in obj && typeof obj.setExpanded === "function";
@@ -383,6 +385,7 @@ export class InputController {
383
385
  this.ctx.isBashMode = trimmed.startsWith("!");
384
386
  this.ctx.isBashNoContext = trimmed.startsWith("!!");
385
387
  this.ctx.isPythonMode = trimmed.startsWith("$") && !trimmed.startsWith("${");
388
+ this.#clearPendingImagesIfPlaceholdersRemoved(text);
386
389
  if (
387
390
  wasBashMode !== this.ctx.isBashMode ||
388
391
  wasBashNoContext !== this.ctx.isBashNoContext ||
@@ -418,7 +421,7 @@ export class InputController {
418
421
  }
419
422
 
420
423
  const runner = this.ctx.session.extensionRunner;
421
- let inputImages = this.ctx.pendingImages.length > 0 ? [...this.ctx.pendingImages] : undefined;
424
+ let inputImages = this.#visiblePendingImagesForText(text);
422
425
 
423
426
  if (runner?.hasHandlers("input")) {
424
427
  const result = await runner.emitInput(text, inputImages, "interactive");
@@ -498,7 +501,7 @@ export class InputController {
498
501
 
499
502
  // Queue input during compaction
500
503
  if (this.ctx.session.isCompacting) {
501
- if (this.ctx.pendingImages.length > 0) {
504
+ if ((inputImages?.length ?? 0) > 0) {
502
505
  this.ctx.showStatus("Compaction in progress. Retry after it completes to send images.");
503
506
  return;
504
507
  }
@@ -520,11 +523,13 @@ export class InputController {
520
523
  // typed since queuing intact. Same protection as #783, applied to
521
524
  // the streaming/queue path.
522
525
  const streamingBehavior = this.#busyStreamingBehavior();
523
- await this.ctx.withLocalSubmission(
524
- text,
525
- () => this.ctx.session.prompt(text, { streamingBehavior, images }),
526
- { imageCount: images?.length ?? 0 },
527
- );
526
+ const promptOptions =
527
+ streamingBehavior === "followUp"
528
+ ? { streamingBehavior, images, followUpQueuePolicy: "sequential" as const }
529
+ : { streamingBehavior, images };
530
+ await this.ctx.withLocalSubmission(text, () => this.ctx.session.prompt(text, promptOptions), {
531
+ imageCount: images?.length ?? 0,
532
+ });
528
533
  this.ctx.updatePendingMessagesDisplay();
529
534
  this.ctx.ui.requestRender();
530
535
  return;
@@ -869,7 +874,9 @@ export class InputController {
869
874
  details,
870
875
  attribution: "user",
871
876
  },
872
- { streamingBehavior },
877
+ streamingBehavior === "followUp"
878
+ ? { streamingBehavior, followUpQueuePolicy: "sequential" }
879
+ : { streamingBehavior },
873
880
  );
874
881
  }
875
882
  if (this.ctx.session.isStreaming) {
@@ -911,7 +918,10 @@ export class InputController {
911
918
  this.ctx.editor.addToHistory(text);
912
919
  this.ctx.editor.setText("");
913
920
  await this.ctx.withLocalSubmission(text, () =>
914
- this.ctx.session.prompt(text, { streamingBehavior: "followUp" }),
921
+ this.ctx.session.prompt(text, {
922
+ streamingBehavior: "followUp",
923
+ followUpQueuePolicy: "sequential",
924
+ }),
915
925
  );
916
926
  this.ctx.updatePendingMessagesDisplay();
917
927
  this.ctx.ui.requestRender();
@@ -1086,7 +1096,7 @@ export class InputController {
1086
1096
  data: image.data,
1087
1097
  mimeType: image.mimeType,
1088
1098
  });
1089
- this.ctx.editor.insertText(`${this.#nextImagePlaceholder()} `);
1099
+ this.ctx.editor.insertText(`${formatPastedImageReference(this.#nextImagePlaceholder(), image.resolvedPath)} `);
1090
1100
  this.ctx.showStatus(`Attached image: ${path.basename(image.resolvedPath)}`, { dim: true });
1091
1101
  this.ctx.ui.requestRender();
1092
1102
  return true;
@@ -1104,6 +1114,37 @@ export class InputController {
1104
1114
  return `[image ${this.ctx.pendingImages.length}]`;
1105
1115
  }
1106
1116
 
1117
+ #visiblePendingImagesForText(text: string): InteractiveModeContext["pendingImages"] | undefined {
1118
+ if (this.ctx.pendingImages.length === 0) {
1119
+ return undefined;
1120
+ }
1121
+
1122
+ const images: InteractiveModeContext["pendingImages"] = [];
1123
+ const seenImageIndexes = new Set<number>();
1124
+ for (const match of text.matchAll(IMAGE_PLACEHOLDER_PATTERN)) {
1125
+ const placeholderNumberText = match[1];
1126
+ if (!placeholderNumberText) continue;
1127
+ const placeholderNumber = Number.parseInt(placeholderNumberText, 10);
1128
+ const imageIndex = placeholderNumber - 1;
1129
+ if (imageIndex < 0 || imageIndex >= this.ctx.pendingImages.length || seenImageIndexes.has(imageIndex)) {
1130
+ continue;
1131
+ }
1132
+ const image = this.ctx.pendingImages[imageIndex];
1133
+ if (!image) continue;
1134
+ images.push(image);
1135
+ seenImageIndexes.add(imageIndex);
1136
+ }
1137
+
1138
+ return images.length > 0 ? images : undefined;
1139
+ }
1140
+
1141
+ #clearPendingImagesIfPlaceholdersRemoved(text: string): void {
1142
+ if (this.ctx.pendingImages.length === 0 || IMAGE_PLACEHOLDER_PRESENT_PATTERN.test(text)) {
1143
+ return;
1144
+ }
1145
+ this.ctx.pendingImages = [];
1146
+ }
1147
+
1107
1148
  async handleImagePaste(): Promise<boolean> {
1108
1149
  try {
1109
1150
  const image = await readImageFromClipboard();
@@ -443,12 +443,13 @@ export class SelectorController {
443
443
  const selector = new ThinkingSelectorComponent(
444
444
  this.ctx.session.thinkingLevel,
445
445
  availableLevels,
446
- level => {
446
+ selection => {
447
447
  done();
448
448
 
449
+ const { level, persistDefault } = selection;
449
450
  const configuredDefault = this.ctx.settings.get("defaultThinkingLevel");
450
451
  const levelToApply = level === ThinkingLevel.Inherit ? configuredDefault : level;
451
- this.ctx.session.setThinkingLevel(levelToApply, false);
452
+ this.ctx.session.setThinkingLevel(levelToApply, persistDefault);
452
453
  const effectiveLevel = this.ctx.session.thinkingLevel ?? ThinkingLevel.Off;
453
454
  const requestedLabel =
454
455
  level === ThinkingLevel.Inherit ? `${level} (configured default: ${configuredDefault})` : level;
@@ -458,9 +459,11 @@ export class SelectorController {
458
459
  this.ctx.statusLine.invalidate();
459
460
  this.ctx.updateEditorBorderColor();
460
461
  this.ctx.updateEditorTopBorder();
462
+ if (persistDefault) void this.ctx.notifyConfigChanged?.();
461
463
  this.ctx.ui.requestRender();
464
+ const scopeLabel = persistDefault ? "Default reasoning effort" : "Reasoning effort";
462
465
  this.ctx.showStatus(
463
- `Reasoning effort set to ${requestedLabel}. Effective effort: ${effectiveLevel}.${clampedSuffix}`,
466
+ `${scopeLabel} set to ${requestedLabel}. Effective effort: ${effectiveLevel}.${clampedSuffix}`,
464
467
  );
465
468
  },
466
469
  () => {
@@ -468,10 +471,9 @@ export class SelectorController {
468
471
  this.ctx.ui.requestRender();
469
472
  },
470
473
  );
471
- return { component: selector, focus: selector.getSelectList() };
474
+ return { component: selector, focus: selector };
472
475
  });
473
476
  }
474
-
475
477
  showSettingsSelector(): void {
476
478
  getAvailableThemes().then(availableThemes => {
477
479
  this.showSelector(done => {
@@ -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();
@@ -34,6 +34,7 @@ import type { Theme } from "./theme/theme";
34
34
  export type CompactionQueuedMessage = {
35
35
  text: string;
36
36
  mode: "steer" | "followUp";
37
+ followUpQueuePolicy?: "sequential";
37
38
  };
38
39
 
39
40
  export type SubmittedUserInput = {