@gajae-code/coding-agent 0.7.11 → 0.8.0

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 (78) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/README.md +1 -1
  3. package/dist/types/capability/mcp.d.ts +2 -0
  4. package/dist/types/config/settings-schema.d.ts +25 -2
  5. package/dist/types/discovery/cursor.d.ts +4 -1
  6. package/dist/types/discovery/index.d.ts +0 -1
  7. package/dist/types/discovery/windsurf.d.ts +3 -1
  8. package/dist/types/gjc-runtime/session-state-sidecar.d.ts +5 -0
  9. package/dist/types/modes/components/custom-editor.d.ts +4 -0
  10. package/dist/types/modes/components/settings-selector.d.ts +1 -0
  11. package/dist/types/modes/components/status-line.d.ts +8 -0
  12. package/dist/types/modes/components/two-column-body.d.ts +9 -0
  13. package/dist/types/modes/components/welcome.d.ts +9 -1
  14. package/dist/types/modes/controllers/input-controller.d.ts +1 -1
  15. package/dist/types/modes/controllers/selector-controller.d.ts +8 -0
  16. package/dist/types/modes/interactive-mode.d.ts +1 -0
  17. package/dist/types/modes/rpc/rpc-types.d.ts +1 -0
  18. package/dist/types/modes/types.d.ts +1 -0
  19. package/dist/types/runtime-mcp/config-writer.d.ts +9 -0
  20. package/dist/types/runtime-mcp/config.d.ts +2 -0
  21. package/dist/types/runtime-mcp/loader.d.ts +2 -0
  22. package/dist/types/runtime-mcp/manager.d.ts +2 -0
  23. package/dist/types/runtime-mcp/types.d.ts +6 -0
  24. package/dist/types/system-prompt.d.ts +7 -0
  25. package/dist/types/workflow/workflow-intent-diff.d.ts +19 -0
  26. package/package.json +10 -10
  27. package/src/capability/mcp.ts +2 -0
  28. package/src/config/mcp-schema.json +5 -1
  29. package/src/config/settings-schema.ts +18 -2
  30. package/src/coordinator-mcp/server.ts +6 -0
  31. package/src/discovery/builtin.ts +12 -0
  32. package/src/discovery/cursor.ts +5 -84
  33. package/src/discovery/gemini.ts +3 -89
  34. package/src/discovery/index.ts +4 -1
  35. package/src/discovery/mcp-json.ts +11 -0
  36. package/src/discovery/opencode.ts +5 -122
  37. package/src/discovery/windsurf.ts +4 -82
  38. package/src/edit/read-file.ts +9 -1
  39. package/src/gjc-runtime/session-state-sidecar.ts +167 -24
  40. package/src/gjc-runtime/team-runtime.ts +56 -51
  41. package/src/internal-urls/docs-index.generated.ts +4 -4
  42. package/src/modes/components/agent-dashboard.ts +1 -34
  43. package/src/modes/components/custom-editor.ts +18 -0
  44. package/src/modes/components/extensions/extension-dashboard.ts +2 -48
  45. package/src/modes/components/model-selector.ts +58 -2
  46. package/src/modes/components/session-selector.ts +19 -9
  47. package/src/modes/components/settings-selector.ts +13 -0
  48. package/src/modes/components/status-line.ts +215 -86
  49. package/src/modes/components/tree-selector.ts +28 -8
  50. package/src/modes/components/two-column-body.ts +36 -0
  51. package/src/modes/components/welcome.ts +171 -35
  52. package/src/modes/controllers/event-controller.ts +4 -0
  53. package/src/modes/controllers/input-controller.ts +151 -30
  54. package/src/modes/controllers/selector-controller.ts +25 -28
  55. package/src/modes/interactive-mode.ts +20 -23
  56. package/src/modes/rpc/rpc-types.ts +1 -0
  57. package/src/modes/shared/agent-wire/deep-interview-gate.ts +9 -3
  58. package/src/modes/shared/agent-wire/workflow-gate-schema.ts +20 -0
  59. package/src/modes/types.ts +1 -0
  60. package/src/notifications/index.ts +20 -1
  61. package/src/notifications/lifecycle-control-runtime.ts +12 -3
  62. package/src/prompts/system/system-prompt.md +15 -1
  63. package/src/runtime-mcp/config-writer.ts +28 -0
  64. package/src/runtime-mcp/config.ts +7 -0
  65. package/src/runtime-mcp/loader.ts +3 -0
  66. package/src/runtime-mcp/manager.ts +3 -0
  67. package/src/runtime-mcp/types.ts +6 -0
  68. package/src/sdk.ts +4 -0
  69. package/src/session/agent-session.ts +21 -1
  70. package/src/session/session-manager.ts +58 -18
  71. package/src/system-prompt.ts +8 -0
  72. package/src/task/executor.ts +5 -0
  73. package/src/tools/ast-grep.ts +10 -7
  74. package/src/tools/bash.ts +15 -6
  75. package/src/tools/index.ts +8 -1
  76. package/src/workflow/workflow-intent-diff.ts +199 -0
  77. package/dist/types/discovery/vscode.d.ts +0 -1
  78. package/src/discovery/vscode.ts +0 -105
@@ -23,13 +23,11 @@ import {
23
23
  extractPrintableText,
24
24
  Input,
25
25
  matchesKey,
26
- padding,
27
26
  replaceTabs,
28
27
  resolveTerminalColumns,
29
28
  Spacer,
30
29
  Text,
31
30
  truncateToWidth,
32
- visibleWidth,
33
31
  wrapTextWithAnsi,
34
32
  } from "@gajae-code/tui";
35
33
  import { isEnoent, prompt } from "@gajae-code/utils";
@@ -52,6 +50,7 @@ import { shortenPath } from "../../tools/render-utils";
52
50
  import { theme } from "../theme/theme";
53
51
  import { matchesAppInterrupt } from "../utils/keybinding-matchers";
54
52
  import { DynamicBorder } from "./dynamic-border";
53
+ import { TwoColumnBody } from "./two-column-body";
55
54
 
56
55
  type SourceTabId = "all" | AgentSource;
57
56
  type AgentScope = "project" | "user";
@@ -296,38 +295,6 @@ class AgentInspectorPane implements Component {
296
295
  invalidate(): void {}
297
296
  }
298
297
 
299
- class TwoColumnBody implements Component {
300
- constructor(
301
- private readonly leftPane: AgentListPane,
302
- private readonly rightPane: AgentInspectorPane,
303
- private readonly maxHeight: number,
304
- ) {}
305
-
306
- render(width: number): string[] {
307
- const leftWidth = Math.floor(width * 0.5);
308
- const rightWidth = width - leftWidth - 3;
309
- const leftLines = this.leftPane.render(leftWidth);
310
- const rightLines = this.rightPane.render(rightWidth);
311
- const lineCount = Math.min(this.maxHeight, Math.max(leftLines.length, rightLines.length));
312
- const out: string[] = [];
313
- const separator = theme.fg("dim", ` ${theme.boxSharp.vertical} `);
314
-
315
- for (let i = 0; i < lineCount; i++) {
316
- const left = truncateToWidth(leftLines[i] ?? "", leftWidth);
317
- const leftPadded = left + padding(Math.max(0, leftWidth - visibleWidth(left)));
318
- const right = truncateToWidth(rightLines[i] ?? "", rightWidth);
319
- out.push(leftPadded + separator + right);
320
- }
321
-
322
- return out;
323
- }
324
-
325
- invalidate(): void {
326
- this.leftPane.invalidate?.();
327
- this.rightPane.invalidate?.();
328
- }
329
- }
330
-
331
298
  export class AgentDashboard extends Container {
332
299
  #settingsManager: Settings | null = null;
333
300
  #allAgents: DashboardAgent[] = [];
@@ -103,6 +103,10 @@ export class CustomEditor extends Editor {
103
103
  onQueue?: () => void;
104
104
  /** Called when Caps Lock is pressed. */
105
105
  onCapsLock?: () => void;
106
+ /** Called when PageUp/PageDown should scroll the transcript viewport instead of prompt history. */
107
+ onViewportPageScroll?: (direction: -1 | 1) => void;
108
+ /** Called before regular composer input should return the transcript viewport to the live bottom. */
109
+ onViewportFollowLive?: () => void;
106
110
 
107
111
  /** Custom key handlers from extensions and non-built-in app actions. */
108
112
  #customKeyHandlers = new Map<KeyId, () => boolean | undefined>();
@@ -243,6 +247,20 @@ export class CustomEditor extends Editor {
243
247
  return;
244
248
  }
245
249
  }
250
+ if (!this.isShowingAutocomplete()) {
251
+ if (matchesKey(data, "pageUp") && this.onViewportPageScroll) {
252
+ this.onViewportPageScroll(-1);
253
+ return;
254
+ }
255
+ if (matchesKey(data, "pageDown") && this.onViewportPageScroll) {
256
+ this.onViewportPageScroll(1);
257
+ return;
258
+ }
259
+ }
260
+
261
+ if (!matchesKey(data, "pageUp") && !matchesKey(data, "pageDown")) {
262
+ this.onViewportFollowLive?.();
263
+ }
246
264
  // Intercept configured image paste (async - fires and handles result)
247
265
  if (this.#matchesAction(data, "app.clipboard.pasteImage") && this.onPasteImage) {
248
266
  void this.onPasteImage();
@@ -11,20 +11,12 @@
11
11
  * - Space: Toggle selected item (or master switch)
12
12
  * - Esc: Close dashboard (clears search first if active)
13
13
  */
14
- import {
15
- type Component,
16
- Container,
17
- matchesKey,
18
- padding,
19
- Spacer,
20
- Text,
21
- truncateToWidth,
22
- visibleWidth,
23
- } from "@gajae-code/tui";
14
+ import { Container, matchesKey, Spacer, Text } from "@gajae-code/tui";
24
15
  import { Settings } from "../../../config/settings";
25
16
  import { DynamicBorder } from "../../../modes/components/dynamic-border";
26
17
  import { theme } from "../../../modes/theme/theme";
27
18
  import { matchesAppInterrupt } from "../../../modes/utils/keybinding-matchers";
19
+ import { TwoColumnBody } from "../two-column-body";
28
20
  import { ExtensionList } from "./extension-list";
29
21
  import { InspectorPanel } from "./inspector-panel";
30
22
  import {
@@ -310,41 +302,3 @@ export class ExtensionDashboard extends Container {
310
302
  }
311
303
  }
312
304
  }
313
-
314
- /**
315
- * Two-column body component for side-by-side rendering.
316
- */
317
- class TwoColumnBody implements Component {
318
- constructor(
319
- private readonly leftPane: ExtensionList,
320
- private readonly rightPane: InspectorPanel,
321
- private readonly maxHeight: number,
322
- ) {}
323
-
324
- render(width: number): string[] {
325
- const leftWidth = Math.floor(width * 0.5);
326
- const rightWidth = Math.max(0, width - leftWidth - 3);
327
-
328
- const leftLines = this.leftPane.render(leftWidth);
329
- const rightLines = this.rightPane.render(rightWidth);
330
-
331
- // Limit to maxHeight lines
332
- const numLines = Math.min(this.maxHeight, Math.max(leftLines.length, rightLines.length));
333
- const combined: string[] = [];
334
- const separator = theme.fg("dim", ` ${theme.boxSharp.vertical} `);
335
-
336
- for (let i = 0; i < numLines; i++) {
337
- const left = truncateToWidth(leftLines[i] ?? "", leftWidth);
338
- const leftPadded = left + padding(Math.max(0, leftWidth - visibleWidth(left)));
339
- const right = truncateToWidth(rightLines[i] ?? "", rightWidth);
340
- combined.push(leftPadded + separator + right);
341
- }
342
-
343
- return combined;
344
- }
345
-
346
- invalidate(): void {
347
- this.leftPane.invalidate?.();
348
- this.rightPane.invalidate?.();
349
- }
350
- }
@@ -1030,11 +1030,51 @@ export class ModelSelectorComponent extends Container {
1030
1030
  this.#filterModels(this.#searchInput.getValue());
1031
1031
  }
1032
1032
 
1033
+ /**
1034
+ * "What is this session running right now" summary for the preset landing
1035
+ * header: active preset (when one is applied), the effective current model
1036
+ * and thinking level, and one line per assigned role (default, executor,
1037
+ * planner, critic, architect).
1038
+ */
1039
+ #formatCurrentSessionLines(): string[] {
1040
+ const lines: string[] = [];
1041
+ const parts: string[] = [];
1042
+ if (this.#activeModelProfile) {
1043
+ const profile = this.#getProfileByName(this.#activeModelProfile);
1044
+ const displayName = profile ? getModelProfilePresentation(profile).displayName : this.#activeModelProfile;
1045
+ parts.push(`preset ${theme.fg("accent", displayName)}`);
1046
+ }
1047
+ if (this.#currentModel) {
1048
+ parts.push(this.#formatAssignedModelLabel(this.#currentModel, this.#currentThinkingLevel));
1049
+ }
1050
+ if (parts.length > 0) lines.push(theme.fg("muted", `Current: ${parts.join(" · ")}`));
1051
+ for (const role of PROFILE_ROLE_PREVIEW_ORDER) {
1052
+ const assigned = this.#roles[role];
1053
+ if (!assigned) continue;
1054
+ const label = GJC_MODEL_ASSIGNMENT_TARGETS[role].tag ?? role.toUpperCase();
1055
+ lines.push(
1056
+ theme.fg("dim", ` ${label}: ${this.#formatAssignedModelLabel(assigned.model, assigned.thinkingLevel)}`),
1057
+ );
1058
+ }
1059
+ return lines;
1060
+ }
1061
+
1062
+ #formatAssignedModelLabel(model: Model, thinkingLevel: ThinkingLevel | undefined): string {
1063
+ let label = `${model.provider}/${model.id}`;
1064
+ if (thinkingLevel && thinkingLevel !== ThinkingLevel.Inherit) {
1065
+ label += ` (${getThinkingLevelMetadata(thinkingLevel).label})`;
1066
+ }
1067
+ return label;
1068
+ }
1069
+
1033
1070
  #renderPresetLanding(): void {
1034
1071
  this.#headerContainer.clear();
1035
1072
  this.#tabBar = null;
1036
1073
  this.#listContainer.clear();
1037
1074
  this.#headerContainer.addChild(new Text(theme.fg("accent", "Model presets"), 0, 0));
1075
+ for (const line of this.#formatCurrentSessionLines()) {
1076
+ this.#headerContainer.addChild(new Text(line, 0, 0));
1077
+ }
1038
1078
  const rows = this.#getPresetRows();
1039
1079
  for (let i = 0; i < rows.length; i++) {
1040
1080
  const row = rows[i];
@@ -1065,17 +1105,23 @@ export class ModelSelectorComponent extends Container {
1065
1105
  if (row.kind === "group") {
1066
1106
  const authenticated = this.#isPresetGroupUsable(row.profiles);
1067
1107
  const mark = this.#providerAuthPending ? "…" : authenticated ? "✓" : "✗";
1108
+ const containsActive =
1109
+ this.#activeModelProfile !== undefined &&
1110
+ row.profiles.some(profile => profile.name === this.#activeModelProfile);
1068
1111
  const label = `${mark} ${row.groupId}`;
1069
1112
  const renderedLabel = selected ? theme.fg("accent", label) : authenticated ? label : theme.fg("dim", label);
1070
- this.#listContainer.addChild(new Text(`${prefix}${renderedLabel}`, 0, 0));
1113
+ const activeSuffix = containsActive ? theme.fg("muted", " (current)") : "";
1114
+ this.#listContainer.addChild(new Text(`${prefix}${renderedLabel}${activeSuffix}`, 0, 0));
1071
1115
  continue;
1072
1116
  }
1073
1117
  const presentation = getModelProfilePresentation(row.profile);
1074
1118
  const authenticated = this.#isPresetAuthenticated(row.profile);
1075
1119
  const mark = this.#providerAuthPending ? "…" : authenticated ? "✓" : "✗";
1120
+ const isActive = row.profile.name === this.#activeModelProfile;
1076
1121
  const label = ` ${mark} ${presentation.displayName}`;
1077
1122
  const renderedLabel = selected ? theme.fg("accent", label) : authenticated ? label : theme.fg("dim", label);
1078
- this.#listContainer.addChild(new Text(`${prefix}${renderedLabel}`, 0, 0));
1123
+ const activeSuffix = isActive ? theme.fg("muted", " (current)") : "";
1124
+ this.#listContainer.addChild(new Text(`${prefix}${renderedLabel}${activeSuffix}`, 0, 0));
1079
1125
  }
1080
1126
  if (this.#presetLoginHint) {
1081
1127
  this.#listContainer.addChild(new Spacer(1));
@@ -1538,7 +1584,17 @@ export class ModelSelectorComponent extends Container {
1538
1584
  const missing = this.#getMissingProviders(row.profiles);
1539
1585
  this.#presetLoginHint = `Run ${missing.map(provider => `/login ${provider}`).join(", ")}`;
1540
1586
  this.#renderPresetLanding();
1587
+ return;
1588
+ }
1589
+ // Enter toggles a usable group's expansion so drilling into presets
1590
+ // works without reaching for the arrow keys (right/left still work).
1591
+ if (this.#expandedPresetProviderId === row.groupId) {
1592
+ this.#collapseSelectedPresetProvider();
1593
+ } else {
1594
+ this.#expandSelectedPresetProvider();
1541
1595
  }
1596
+ this.#presetLoginHint = undefined;
1597
+ this.#renderPresetLanding();
1542
1598
  return;
1543
1599
  }
1544
1600
  const missing = this.#getMissingProviders(row.profile);
@@ -50,6 +50,14 @@ class SessionList implements Component {
50
50
  };
51
51
  }
52
52
 
53
+ #clampSelectedIndex(): void {
54
+ if (this.#filteredSessions.length === 0) {
55
+ this.#selectedIndex = 0;
56
+ return;
57
+ }
58
+ this.#selectedIndex = Math.max(0, Math.min(this.#selectedIndex, this.#filteredSessions.length - 1));
59
+ }
60
+
53
61
  #filterSessions(query: string): void {
54
62
  this.#filteredSessions = fuzzyFilter(this.allSessions, query, session => {
55
63
  const parts = [
@@ -62,7 +70,7 @@ class SessionList implements Component {
62
70
  ];
63
71
  return parts.filter(Boolean).join(" ");
64
72
  });
65
- this.#selectedIndex = Math.min(this.#selectedIndex, Math.max(0, this.#filteredSessions.length - 1));
73
+ this.#clampSelectedIndex();
66
74
  }
67
75
 
68
76
  removeSession(sessionPath: string): void {
@@ -71,10 +79,6 @@ class SessionList implements Component {
71
79
  this.allSessions.splice(index, 1);
72
80
  // Re-filter to update filteredSessions
73
81
  this.#filterSessions(this.#searchInput.getValue());
74
- // Adjust selectedIndex if we deleted the last item or beyond
75
- if (this.#selectedIndex >= this.#filteredSessions.length) {
76
- this.#selectedIndex = Math.max(0, this.#filteredSessions.length - 1);
77
- }
78
82
  }
79
83
 
80
84
  invalidate(): void {
@@ -182,6 +186,12 @@ class SessionList implements Component {
182
186
  }
183
187
 
184
188
  handleInput(keyData: string): void {
189
+ const moveSelection = (delta: number): void => {
190
+ if (this.#filteredSessions.length === 0) return;
191
+ this.#selectedIndex += delta;
192
+ this.#clampSelectedIndex();
193
+ };
194
+
185
195
  // Delete key - request delete confirmation from parent
186
196
  if (matchesKey(keyData, "delete")) {
187
197
  const selected = this.#filteredSessions[this.#selectedIndex];
@@ -193,22 +203,22 @@ class SessionList implements Component {
193
203
 
194
204
  // Up arrow
195
205
  if (matchesKey(keyData, "up")) {
196
- this.#selectedIndex = Math.max(0, this.#selectedIndex - 1);
206
+ moveSelection(-1);
197
207
  return;
198
208
  }
199
209
  // Down arrow
200
210
  if (matchesKey(keyData, "down")) {
201
- this.#selectedIndex = Math.min(this.#filteredSessions.length - 1, this.#selectedIndex + 1);
211
+ moveSelection(1);
202
212
  return;
203
213
  }
204
214
  // Page up - jump up by maxVisible items
205
215
  if (matchesKey(keyData, "pageUp")) {
206
- this.#selectedIndex = Math.max(0, this.#selectedIndex - this.#maxVisible);
216
+ moveSelection(-this.#maxVisible);
207
217
  return;
208
218
  }
209
219
  // Page down - jump down by maxVisible items
210
220
  if (matchesKey(keyData, "pageDown")) {
211
- this.#selectedIndex = Math.min(this.#filteredSessions.length - 1, this.#selectedIndex + this.#maxVisible);
221
+ moveSelection(this.#maxVisible);
212
222
  return;
213
223
  }
214
224
  // Enter
@@ -670,6 +670,7 @@ export interface StatusLinePreviewSettings {
670
670
  segmentOptions?: StatusLineSegmentOptions;
671
671
  previewHighlightSegment?: StatusLineSegmentId;
672
672
  sessionAccent?: boolean;
673
+ maxRows?: number;
673
674
  }
674
675
 
675
676
  export interface SettingsCallbacks {
@@ -908,6 +909,18 @@ export class SettingsSelectorComponent extends Container {
908
909
  this.callbacks.onStatusLinePreview?.({ separator, previewHighlightSegment: undefined });
909
910
  this.#updateStatusPreview();
910
911
  };
912
+ } else if (def.path === "statusLine.maxRows") {
913
+ onPreview = value => {
914
+ this.callbacks.onStatusLinePreview?.({ maxRows: Number(value) });
915
+ this.#updateStatusPreview();
916
+ };
917
+ onPreviewCancel = () => {
918
+ this.callbacks.onStatusLinePreview?.({
919
+ maxRows: settings.get("statusLine.maxRows"),
920
+ previewHighlightSegment: undefined,
921
+ });
922
+ this.#updateStatusPreview();
923
+ };
911
924
  }
912
925
 
913
926
  // Provide status line preview for theme selection