@gajae-code/coding-agent 0.7.11 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +47 -0
- package/README.md +1 -1
- package/dist/types/capability/mcp.d.ts +2 -0
- package/dist/types/config/settings-schema.d.ts +25 -2
- package/dist/types/discovery/cursor.d.ts +4 -1
- package/dist/types/discovery/index.d.ts +0 -1
- package/dist/types/discovery/windsurf.d.ts +3 -1
- package/dist/types/gjc-runtime/session-state-sidecar.d.ts +5 -0
- package/dist/types/modes/components/custom-editor.d.ts +4 -0
- package/dist/types/modes/components/settings-selector.d.ts +1 -0
- package/dist/types/modes/components/status-line.d.ts +8 -0
- package/dist/types/modes/components/two-column-body.d.ts +9 -0
- package/dist/types/modes/components/welcome.d.ts +9 -1
- package/dist/types/modes/controllers/input-controller.d.ts +1 -1
- package/dist/types/modes/controllers/selector-controller.d.ts +8 -0
- package/dist/types/modes/interactive-mode.d.ts +1 -0
- package/dist/types/modes/rpc/rpc-types.d.ts +1 -0
- package/dist/types/modes/types.d.ts +1 -0
- package/dist/types/notifications/telegram-daemon.d.ts +3 -0
- package/dist/types/runtime-mcp/config-writer.d.ts +9 -0
- package/dist/types/runtime-mcp/config.d.ts +2 -0
- package/dist/types/runtime-mcp/loader.d.ts +2 -0
- package/dist/types/runtime-mcp/manager.d.ts +2 -0
- package/dist/types/runtime-mcp/types.d.ts +6 -0
- package/dist/types/system-prompt.d.ts +7 -0
- package/dist/types/tools/skill.d.ts +1 -1
- package/dist/types/workflow/workflow-intent-diff.d.ts +19 -0
- package/package.json +10 -10
- package/src/capability/mcp.ts +2 -0
- package/src/config/mcp-schema.json +5 -1
- package/src/config/settings-schema.ts +18 -2
- package/src/coordinator-mcp/server.ts +39 -4
- package/src/discovery/builtin.ts +12 -0
- package/src/discovery/cursor.ts +5 -84
- package/src/discovery/gemini.ts +3 -89
- package/src/discovery/index.ts +4 -1
- package/src/discovery/mcp-json.ts +11 -0
- package/src/discovery/opencode.ts +5 -122
- package/src/discovery/windsurf.ts +4 -82
- package/src/edit/read-file.ts +9 -1
- package/src/extensibility/skills.ts +30 -2
- package/src/gjc-runtime/session-state-sidecar.ts +167 -24
- package/src/gjc-runtime/team-runtime.ts +56 -51
- package/src/internal-urls/docs-index.generated.ts +4 -4
- package/src/modes/acp/acp-agent.ts +7 -6
- package/src/modes/components/agent-dashboard.ts +1 -34
- package/src/modes/components/custom-editor.ts +18 -0
- package/src/modes/components/extensions/extension-dashboard.ts +2 -48
- package/src/modes/components/model-selector.ts +58 -2
- package/src/modes/components/session-selector.ts +19 -9
- package/src/modes/components/settings-selector.ts +13 -0
- package/src/modes/components/status-line.ts +215 -86
- package/src/modes/components/tree-selector.ts +28 -8
- package/src/modes/components/two-column-body.ts +36 -0
- package/src/modes/components/welcome.ts +171 -35
- package/src/modes/controllers/command-controller.ts +3 -2
- package/src/modes/controllers/event-controller.ts +4 -0
- package/src/modes/controllers/input-controller.ts +158 -38
- package/src/modes/controllers/selector-controller.ts +25 -28
- package/src/modes/interactive-mode.ts +20 -23
- package/src/modes/prompt-action-autocomplete.ts +17 -7
- package/src/modes/rpc/rpc-types.ts +1 -0
- package/src/modes/shared/agent-wire/deep-interview-gate.ts +9 -3
- package/src/modes/shared/agent-wire/workflow-gate-schema.ts +20 -0
- package/src/modes/types.ts +1 -0
- package/src/notifications/index.ts +20 -1
- package/src/notifications/lifecycle-control-runtime.ts +12 -3
- package/src/notifications/telegram-daemon.ts +14 -0
- package/src/prompts/system/system-prompt.md +15 -1
- package/src/runtime-mcp/config-writer.ts +28 -0
- package/src/runtime-mcp/config.ts +7 -0
- package/src/runtime-mcp/loader.ts +3 -0
- package/src/runtime-mcp/manager.ts +3 -0
- package/src/runtime-mcp/types.ts +6 -0
- package/src/sdk.ts +4 -0
- package/src/session/agent-session.ts +21 -1
- package/src/session/session-manager.ts +58 -18
- package/src/system-prompt.ts +8 -0
- package/src/task/executor.ts +5 -0
- package/src/tools/ast-grep.ts +10 -7
- package/src/tools/bash.ts +15 -6
- package/src/tools/index.ts +8 -1
- package/src/tools/skill.ts +1 -1
- package/src/workflow/workflow-intent-diff.ts +199 -0
- package/dist/types/discovery/vscode.d.ts +0 -1
- package/src/discovery/vscode.ts +0 -105
|
@@ -14,6 +14,12 @@ export interface LspServerInfo {
|
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
export type WelcomeLogoMode = "unicode" | "square" | "ascii";
|
|
17
|
+
export interface WelcomeComponentOptions {
|
|
18
|
+
getViewportRows?: () => number | undefined;
|
|
19
|
+
getReservedBottomRows?: (termWidth: number) => number;
|
|
20
|
+
changelogMarkdown?: string;
|
|
21
|
+
collapseChangelog?: boolean;
|
|
22
|
+
}
|
|
17
23
|
|
|
18
24
|
/**
|
|
19
25
|
* GJC-native launch surface with compact command affordances, project
|
|
@@ -30,6 +36,7 @@ export class WelcomeComponent implements Component {
|
|
|
30
36
|
private recentSessions: RecentSession[] = [],
|
|
31
37
|
private lspServers: LspServerInfo[] = [],
|
|
32
38
|
private readonly logoMode: WelcomeLogoMode = "unicode",
|
|
39
|
+
private readonly options: WelcomeComponentOptions = {},
|
|
33
40
|
) {}
|
|
34
41
|
|
|
35
42
|
invalidate(): void {}
|
|
@@ -50,6 +57,11 @@ export class WelcomeComponent implements Component {
|
|
|
50
57
|
}
|
|
51
58
|
requestRender();
|
|
52
59
|
}, INTRO_TICK_MS);
|
|
60
|
+
this.#animTimer.unref?.();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
dispose(): void {
|
|
64
|
+
this.#stopAnimation();
|
|
53
65
|
}
|
|
54
66
|
|
|
55
67
|
#stopAnimation(): void {
|
|
@@ -74,15 +86,20 @@ export class WelcomeComponent implements Component {
|
|
|
74
86
|
}
|
|
75
87
|
|
|
76
88
|
render(termWidth: number): string[] {
|
|
77
|
-
|
|
78
|
-
const boxWidth = Math.max(0, termWidth - 2);
|
|
89
|
+
const boxWidth = Math.max(0, termWidth);
|
|
79
90
|
if (boxWidth < 4) {
|
|
80
91
|
return [];
|
|
81
92
|
}
|
|
93
|
+
|
|
94
|
+
const targetRows = this.#targetRows(termWidth);
|
|
95
|
+
if (targetRows !== undefined && targetRows <= 0) {
|
|
96
|
+
return [];
|
|
97
|
+
}
|
|
98
|
+
const targetContentRows = targetRows === undefined ? undefined : Math.max(0, targetRows - 2);
|
|
82
99
|
const dualContentWidth = boxWidth - 3; // 3 = │ + │ + │
|
|
83
|
-
const preferredLeftCol =
|
|
84
|
-
const minLeftCol =
|
|
85
|
-
const minRightCol =
|
|
100
|
+
const preferredLeftCol = 40;
|
|
101
|
+
const minLeftCol = 20; // logo mark plus GJC identity labels
|
|
102
|
+
const minRightCol = 24;
|
|
86
103
|
const modelPill = this.#pill(theme.icon.model || "model", this.modelName, "statusLineModel");
|
|
87
104
|
const providerPill = this.#pill(theme.icon.package || "provider", this.providerName, "statusLinePath");
|
|
88
105
|
const logoLines = this.#logoLines();
|
|
@@ -90,12 +107,12 @@ export class WelcomeComponent implements Component {
|
|
|
90
107
|
const leftMinContentWidth = Math.max(
|
|
91
108
|
minLeftCol,
|
|
92
109
|
logoMinWidth,
|
|
93
|
-
visibleWidth("
|
|
110
|
+
visibleWidth("GJC Forge"),
|
|
94
111
|
visibleWidth("shape · act · prove"),
|
|
95
112
|
visibleWidth(modelPill),
|
|
96
113
|
visibleWidth(providerPill),
|
|
97
114
|
);
|
|
98
|
-
const desiredLeftCol = Math.min(preferredLeftCol, Math.max(minLeftCol, Math.floor(dualContentWidth * 0.
|
|
115
|
+
const desiredLeftCol = Math.min(preferredLeftCol, Math.max(minLeftCol, Math.floor(dualContentWidth * 0.38)));
|
|
99
116
|
const dualLeftCol =
|
|
100
117
|
dualContentWidth >= minRightCol + 1
|
|
101
118
|
? Math.min(desiredLeftCol, dualContentWidth - minRightCol)
|
|
@@ -107,36 +124,36 @@ export class WelcomeComponent implements Component {
|
|
|
107
124
|
|
|
108
125
|
const logoColored = this.#currentLogoFrame(logoLines);
|
|
109
126
|
|
|
110
|
-
// Left column - centered content
|
|
111
127
|
const leftLines = [
|
|
112
128
|
"",
|
|
113
|
-
this.#centerText(theme.bold(theme.fg("accent", "
|
|
129
|
+
this.#centerText(theme.bold(theme.fg("accent", "GJC Forge")), leftCol),
|
|
114
130
|
this.#centerText(theme.fg("dim", "shape · act · prove"), leftCol),
|
|
115
131
|
"",
|
|
116
132
|
...logoColored.map(l => this.#centerText(l, leftCol)),
|
|
117
133
|
"",
|
|
134
|
+
this.#centerText(this.#loadingLine(), leftCol),
|
|
118
135
|
this.#centerText(modelPill, leftCol),
|
|
119
136
|
this.#centerText(providerPill, leftCol),
|
|
137
|
+
"",
|
|
120
138
|
];
|
|
121
139
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
140
|
+
const buildSeparator = (columnWidth: number): string =>
|
|
141
|
+
` ${theme.fg("dim", theme.boxRound.horizontal.repeat(Math.max(0, columnWidth - 2)))}`;
|
|
142
|
+
|
|
143
|
+
const rightColumnWidth = showRightColumn ? rightCol : leftCol;
|
|
144
|
+
const separator = buildSeparator(rightColumnWidth);
|
|
145
|
+
const changelogBudget = Math.max(1, Math.min(6, Math.floor((targetContentRows ?? 18) * 0.3)));
|
|
125
146
|
|
|
126
|
-
// Recent sessions content
|
|
127
147
|
const sessionLines: string[] = [];
|
|
128
148
|
if (this.recentSessions.length === 0) {
|
|
129
149
|
sessionLines.push(` ${theme.fg("dim", "No saved trails")}`);
|
|
130
150
|
} else {
|
|
131
|
-
// Reserve width for the bullet prefix (" • ") and the trailing " (timeAgo)"
|
|
132
|
-
// so the relative time is never the part that gets truncated. The name
|
|
133
|
-
// absorbs whatever space is left.
|
|
134
151
|
const bulletPrefix = ` ${theme.md.bullet} `;
|
|
135
152
|
const prefixWidth = visibleWidth(bulletPrefix);
|
|
136
153
|
for (const session of this.recentSessions.slice(0, 3)) {
|
|
137
154
|
const timeSuffixRaw = ` (${session.timeAgo})`;
|
|
138
155
|
const timeWidth = visibleWidth(timeSuffixRaw);
|
|
139
|
-
const nameBudget = Math.max(1,
|
|
156
|
+
const nameBudget = Math.max(1, rightColumnWidth - prefixWidth - timeWidth);
|
|
140
157
|
const nameVis = visibleWidth(session.name);
|
|
141
158
|
const name = nameVis > nameBudget ? truncateToWidth(session.name, nameBudget) : session.name;
|
|
142
159
|
sessionLines.push(
|
|
@@ -145,12 +162,11 @@ export class WelcomeComponent implements Component {
|
|
|
145
162
|
}
|
|
146
163
|
}
|
|
147
164
|
|
|
148
|
-
// LSP servers content
|
|
149
165
|
const lspLines: string[] = [];
|
|
150
166
|
if (this.lspServers.length === 0) {
|
|
151
167
|
lspLines.push(` ${theme.fg("dim", "No LSP servers")}`);
|
|
152
168
|
} else {
|
|
153
|
-
for (const server of this.lspServers) {
|
|
169
|
+
for (const server of this.lspServers.slice(0, 4)) {
|
|
154
170
|
const icon =
|
|
155
171
|
server.status === "ready"
|
|
156
172
|
? theme.styledSymbol("status.success", "success")
|
|
@@ -162,8 +178,11 @@ export class WelcomeComponent implements Component {
|
|
|
162
178
|
}
|
|
163
179
|
}
|
|
164
180
|
|
|
165
|
-
// Right column
|
|
166
181
|
const rightLines = [
|
|
182
|
+
"",
|
|
183
|
+
` ${theme.bold(theme.fg("accent", "What's New"))}`,
|
|
184
|
+
...this.#whatsNewLines(rightColumnWidth, changelogBudget),
|
|
185
|
+
separator,
|
|
167
186
|
` ${theme.bold(theme.fg("accent", "Flow keys"))}`,
|
|
168
187
|
` ${theme.fg("dim", "/")}${theme.fg("muted", " commands")} ${theme.fg("dim", "·")} ${theme.fg(
|
|
169
188
|
"dim",
|
|
@@ -187,7 +206,12 @@ export class WelcomeComponent implements Component {
|
|
|
187
206
|
"",
|
|
188
207
|
];
|
|
189
208
|
|
|
190
|
-
|
|
209
|
+
const contentRows =
|
|
210
|
+
targetContentRows ??
|
|
211
|
+
(showRightColumn ? Math.max(leftLines.length, rightLines.length) : leftLines.length + rightLines.length);
|
|
212
|
+
const outputRows = targetRows === undefined ? Math.max(3, contentRows + 2) : targetRows;
|
|
213
|
+
const bodyRows = Math.max(0, outputRows - 2);
|
|
214
|
+
|
|
191
215
|
const hChar = theme.boxRound.horizontal;
|
|
192
216
|
const h = theme.fg("dim", hChar);
|
|
193
217
|
const v = theme.fg("dim", theme.boxRound.vertical);
|
|
@@ -197,9 +221,7 @@ export class WelcomeComponent implements Component {
|
|
|
197
221
|
const br = theme.fg("dim", theme.boxRound.bottomRight);
|
|
198
222
|
|
|
199
223
|
const lines: string[] = [];
|
|
200
|
-
|
|
201
|
-
// Top border with embedded title
|
|
202
|
-
const title = ` ${APP_NAME} v${this.version} · GJC forge `;
|
|
224
|
+
const title = ` ${APP_NAME} v${this.version} · GJC Forge `;
|
|
203
225
|
const titlePrefixRaw = hChar.repeat(3);
|
|
204
226
|
const titleStyled = theme.fg("dim", titlePrefixRaw) + theme.fg("muted", title);
|
|
205
227
|
const titleVisLen = visibleWidth(titlePrefixRaw) + visibleWidth(title);
|
|
@@ -210,22 +232,32 @@ export class WelcomeComponent implements Component {
|
|
|
210
232
|
const afterTitle = titleSpace - titleVisLen;
|
|
211
233
|
lines.push(tl + titleStyled + theme.fg("dim", hChar.repeat(afterTitle)) + tr);
|
|
212
234
|
}
|
|
235
|
+
if (outputRows === 1) {
|
|
236
|
+
return lines;
|
|
237
|
+
}
|
|
213
238
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
const right = this.#fitToWidth(
|
|
239
|
+
if (showRightColumn) {
|
|
240
|
+
const leftBlock = this.#fitBlock(leftLines, bodyRows, "center");
|
|
241
|
+
const rightBlock = this.#fitBlock(rightLines, bodyRows, "top");
|
|
242
|
+
for (let i = 0; i < bodyRows; i++) {
|
|
243
|
+
const left = this.#fitToWidth(leftBlock[i] ?? "", leftCol);
|
|
244
|
+
const right = this.#fitToWidth(rightBlock[i] ?? "", rightCol);
|
|
220
245
|
lines.push(v + left + v + right + v);
|
|
221
|
-
} else {
|
|
222
|
-
lines.push(v + left + v);
|
|
223
246
|
}
|
|
224
|
-
}
|
|
225
|
-
// Bottom border
|
|
226
|
-
if (showRightColumn) {
|
|
227
247
|
lines.push(bl + h.repeat(leftCol) + theme.fg("dim", theme.boxSharp.teeUp) + h.repeat(rightCol) + br);
|
|
228
248
|
} else {
|
|
249
|
+
const compactLeftLines =
|
|
250
|
+
bodyRows < 14
|
|
251
|
+
? [
|
|
252
|
+
this.#centerText(theme.bold(theme.fg("accent", "GJC Forge")), leftCol),
|
|
253
|
+
this.#centerText(this.#loadingLine(), leftCol),
|
|
254
|
+
this.#centerText(modelPill, leftCol),
|
|
255
|
+
]
|
|
256
|
+
: leftLines;
|
|
257
|
+
const singleBlock = this.#fitBlock([...compactLeftLines, separator, ...rightLines], bodyRows, "top");
|
|
258
|
+
for (let i = 0; i < bodyRows; i++) {
|
|
259
|
+
lines.push(v + this.#fitToWidth(singleBlock[i] ?? "", leftCol) + v);
|
|
260
|
+
}
|
|
229
261
|
lines.push(bl + h.repeat(leftCol) + br);
|
|
230
262
|
}
|
|
231
263
|
|
|
@@ -251,6 +283,110 @@ export class WelcomeComponent implements Component {
|
|
|
251
283
|
}
|
|
252
284
|
return str + padding(width - visLen);
|
|
253
285
|
}
|
|
286
|
+
#targetRows(termWidth: number): number | undefined {
|
|
287
|
+
const viewportRows = this.options.getViewportRows?.();
|
|
288
|
+
if (typeof viewportRows !== "number" || !Number.isFinite(viewportRows) || viewportRows <= 0) {
|
|
289
|
+
return undefined;
|
|
290
|
+
}
|
|
291
|
+
const reservedRows = Math.max(0, Math.floor(this.options.getReservedBottomRows?.(termWidth) ?? 0));
|
|
292
|
+
return Math.max(0, Math.floor(viewportRows) - reservedRows);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
#loadingLine(): string {
|
|
296
|
+
const frames = theme.spinnerFrames;
|
|
297
|
+
const elapsed = this.#animStart == null ? 0 : performance.now() - this.#animStart;
|
|
298
|
+
const frame = frames.length > 0 ? (frames[Math.floor(elapsed / 100) % frames.length] ?? "*") : "*";
|
|
299
|
+
const label = this.#animStart == null ? "ready" : "warming workspace";
|
|
300
|
+
return `${theme.fg("warning", frame)} ${theme.fg("muted", label)}`;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
#fitBlock(lines: string[], rows: number, align: "top" | "center"): string[] {
|
|
304
|
+
if (rows <= 0) return [];
|
|
305
|
+
const clipped =
|
|
306
|
+
lines.length > rows
|
|
307
|
+
? rows === 1
|
|
308
|
+
? [theme.fg("dim", " …")]
|
|
309
|
+
: [...lines.slice(0, rows - 1), theme.fg("dim", " …")]
|
|
310
|
+
: lines;
|
|
311
|
+
const missingRows = rows - clipped.length;
|
|
312
|
+
if (missingRows <= 0) return clipped;
|
|
313
|
+
const topPad = align === "center" ? Math.floor(missingRows / 2) : 0;
|
|
314
|
+
const bottomPad = missingRows - topPad;
|
|
315
|
+
return [...Array.from({ length: topPad }, () => ""), ...clipped, ...Array.from({ length: bottomPad }, () => "")];
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
#whatsNewLines(width: number, maxItems: number): string[] {
|
|
319
|
+
const changelog = this.options.changelogMarkdown?.trim();
|
|
320
|
+
if (!changelog) {
|
|
321
|
+
return [` ${theme.fg("dim", "Ready for your next prompt")}`];
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const version = this.#latestChangelogVersion(changelog);
|
|
325
|
+
if (this.options.collapseChangelog) {
|
|
326
|
+
return [
|
|
327
|
+
` ${theme.fg("muted", `Updated to v${version}`)}`,
|
|
328
|
+
` ${theme.fg("dim", `Use ${theme.bold("/changelog")} for details`)}`,
|
|
329
|
+
];
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const itemLimit = Math.max(1, Math.floor(maxItems));
|
|
333
|
+
const items = this.#changelogItems(changelog);
|
|
334
|
+
if (items.length === 0) {
|
|
335
|
+
return [
|
|
336
|
+
` ${theme.fg("muted", `Updated to v${version}`)}`,
|
|
337
|
+
` ${theme.fg("dim", `Use ${theme.bold("/changelog")} for details`)}`,
|
|
338
|
+
];
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const prefix = ` ${theme.md.bullet} `;
|
|
342
|
+
const textWidth = Math.max(1, width - visibleWidth(prefix));
|
|
343
|
+
const visibleItems = items.slice(0, itemLimit).map(item => {
|
|
344
|
+
const text = visibleWidth(item) > textWidth ? truncateToWidth(item, textWidth) : item;
|
|
345
|
+
return `${theme.fg("dim", prefix)}${theme.fg("muted", text)}`;
|
|
346
|
+
});
|
|
347
|
+
if (items.length > visibleItems.length) {
|
|
348
|
+
visibleItems.push(` ${theme.fg("dim", `… ${theme.bold("/changelog")} for full notes`)}`);
|
|
349
|
+
}
|
|
350
|
+
return visibleItems;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
#latestChangelogVersion(markdown: string): string {
|
|
354
|
+
const versionMatch = markdown.match(/##\s+\[?(\d+\.\d+\.\d+)\]?/);
|
|
355
|
+
return versionMatch?.[1] ?? this.version;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
#changelogItems(markdown: string): string[] {
|
|
359
|
+
const items: string[] = [];
|
|
360
|
+
let inFence = false;
|
|
361
|
+
for (const rawLine of markdown.split(/\r?\n/)) {
|
|
362
|
+
const line = rawLine.trim();
|
|
363
|
+
if (line.startsWith("```")) {
|
|
364
|
+
inFence = !inFence;
|
|
365
|
+
continue;
|
|
366
|
+
}
|
|
367
|
+
if (inFence || !line || /^#{1,6}\s+/.test(line) || /^-{3,}$/.test(line)) {
|
|
368
|
+
continue;
|
|
369
|
+
}
|
|
370
|
+
const withoutBullet = line
|
|
371
|
+
.replace(/^[-*]\s+/, "")
|
|
372
|
+
.replace(/^\d+\.\s+/, "")
|
|
373
|
+
.replace(/^>\s*/, "");
|
|
374
|
+
const cleaned = this.#stripMarkdown(withoutBullet);
|
|
375
|
+
if (cleaned) items.push(cleaned);
|
|
376
|
+
}
|
|
377
|
+
return items;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
#stripMarkdown(text: string): string {
|
|
381
|
+
return text
|
|
382
|
+
.replace(/`([^`]+)`/g, "$1")
|
|
383
|
+
.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1")
|
|
384
|
+
.replace(/\*\*([^*]+)\*\*/g, "$1")
|
|
385
|
+
.replace(/__([^_]+)__/g, "$1")
|
|
386
|
+
.replace(/\*([^*]+)\*/g, "$1")
|
|
387
|
+
.replace(/[_~]/g, "")
|
|
388
|
+
.trim();
|
|
389
|
+
}
|
|
254
390
|
|
|
255
391
|
#pill(icon: string, text: string, color: ThemeColor): string {
|
|
256
392
|
return `${theme.fg("borderMuted", "[")} ${theme.fg(color, icon)} ${theme.fg("muted", text)} ${theme.fg(
|
|
@@ -1276,17 +1276,18 @@ export class CommandController {
|
|
|
1276
1276
|
async handleContributionPrepCommand(customInstructions?: string): Promise<void> {
|
|
1277
1277
|
this.ctx.editor.setText("");
|
|
1278
1278
|
try {
|
|
1279
|
-
const result = await this.ctx.session.prepareContributionPrep({ customInstructions, spawnWorker:
|
|
1279
|
+
const result = await this.ctx.session.prepareContributionPrep({ customInstructions, spawnWorker: false });
|
|
1280
1280
|
this.ctx.showStatus(
|
|
1281
1281
|
[
|
|
1282
1282
|
"Contribution prep artifacts written.",
|
|
1283
1283
|
`Manifest: ${result.manifestPath}`,
|
|
1284
1284
|
`Worker prompt: ${result.workerPromptPath}`,
|
|
1285
|
+
"Open the worker prompt from a separate terminal to keep this TUI session stable.",
|
|
1285
1286
|
].join("\n"),
|
|
1286
1287
|
);
|
|
1287
1288
|
this.ctx.chatContainer.addChild(
|
|
1288
1289
|
new Text(
|
|
1289
|
-
`${theme.fg("accent", `${theme.status.success} Contribution prep ready`)}\
|
|
1290
|
+
`${theme.fg("accent", `${theme.status.success} Contribution prep ready`)}\nWorker prompt: ${result.workerPromptPath}`,
|
|
1290
1291
|
1,
|
|
1291
1292
|
1,
|
|
1292
1293
|
),
|
|
@@ -138,6 +138,7 @@ export class EventController {
|
|
|
138
138
|
this.ctx.editor.onEscape = this.ctx.retryEscapeHandler;
|
|
139
139
|
this.ctx.retryEscapeHandler = undefined;
|
|
140
140
|
}
|
|
141
|
+
this.ctx.retryEscapePrimed = false;
|
|
141
142
|
if (this.ctx.retryLoader) {
|
|
142
143
|
this.ctx.retryLoader.stop();
|
|
143
144
|
this.ctx.retryLoader = undefined;
|
|
@@ -241,6 +242,7 @@ export class EventController {
|
|
|
241
242
|
this.ctx.retryLoader = undefined;
|
|
242
243
|
this.ctx.statusContainer.clear();
|
|
243
244
|
}
|
|
245
|
+
this.ctx.retryEscapePrimed = false;
|
|
244
246
|
this.#cancelIdleCompaction();
|
|
245
247
|
this.ctx.updateEditorBorderColor();
|
|
246
248
|
this.ctx.ensureLoadingAnimation();
|
|
@@ -768,6 +770,7 @@ export class EventController {
|
|
|
768
770
|
if (!this.ctx.retryEscapeHandler) {
|
|
769
771
|
this.ctx.retryEscapeHandler = this.ctx.editor.onEscape;
|
|
770
772
|
}
|
|
773
|
+
this.ctx.retryEscapePrimed = false;
|
|
771
774
|
let escPressed = false;
|
|
772
775
|
this.ctx.editor.onEscape = () => {
|
|
773
776
|
if (!escPressed) {
|
|
@@ -818,6 +821,7 @@ export class EventController {
|
|
|
818
821
|
this.ctx.retryLoader = undefined;
|
|
819
822
|
this.ctx.statusContainer.clear();
|
|
820
823
|
}
|
|
824
|
+
this.ctx.retryEscapePrimed = false;
|
|
821
825
|
if (!event.success) {
|
|
822
826
|
this.ctx.showError(`Retry failed after ${event.attempt} attempts: ${event.finalError || "Unknown error"}`);
|
|
823
827
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as fs from "node:fs/promises";
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import { type AgentMessage, ThinkingLevel } from "@gajae-code/agent-core";
|
|
4
|
-
import type
|
|
4
|
+
import { type AutocompleteProvider, matchesKey, type SlashCommand } from "@gajae-code/tui";
|
|
5
5
|
import { $env, sanitizeText } from "@gajae-code/utils";
|
|
6
6
|
import { isSettingsInitialized, settings } from "../../config/settings";
|
|
7
7
|
import { resolveSubskillActivationForSkillInvocation } from "../../extensibility/gjc-plugins";
|
|
@@ -42,6 +42,127 @@ export class InputController {
|
|
|
42
42
|
* so abort cleanup going idle cannot turn the second Esc into an idle action. */
|
|
43
43
|
#steerConsumePending = false;
|
|
44
44
|
|
|
45
|
+
#globalInterruptUnsubscribe: (() => void) | undefined;
|
|
46
|
+
|
|
47
|
+
#matchesInterruptKey(data: string): boolean {
|
|
48
|
+
return this.ctx.keybindings.getKeys("app.interrupt").some(key => matchesKey(data, key));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
#hasHookDialog(): boolean {
|
|
52
|
+
return Boolean(this.ctx.hookSelector || this.ctx.hookInput || this.ctx.hookEditor);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
#isRetryBackoffActive(): boolean {
|
|
56
|
+
return Boolean(
|
|
57
|
+
this.ctx.retryLoader ||
|
|
58
|
+
this.ctx.retryEscapeHandler ||
|
|
59
|
+
(this.ctx.session.isRetrying && !this.ctx.session.isStreaming),
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
#handleCancellableWorkEscape(options: {
|
|
64
|
+
loading?: boolean;
|
|
65
|
+
processes?: boolean;
|
|
66
|
+
modes?: boolean;
|
|
67
|
+
maintenance?: boolean;
|
|
68
|
+
retry?: boolean;
|
|
69
|
+
streaming?: boolean;
|
|
70
|
+
}): boolean {
|
|
71
|
+
if (options.loading && this.ctx.loadingAnimation) {
|
|
72
|
+
if (this.ctx.cancelPendingSubmission()) {
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
this.restoreQueuedMessagesToEditor({ abort: true });
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
if (options.processes && this.ctx.session.isBashRunning) {
|
|
79
|
+
this.ctx.session.abortBash();
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
if (options.modes && this.ctx.isBashMode) {
|
|
83
|
+
this.ctx.editor.setText("");
|
|
84
|
+
this.ctx.isBashMode = false;
|
|
85
|
+
this.ctx.isBashNoContext = false;
|
|
86
|
+
this.ctx.updateEditorBorderColor();
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
if (options.processes && this.ctx.session.isEvalRunning) {
|
|
90
|
+
this.ctx.session.abortEval();
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
if (options.modes && this.ctx.isPythonMode) {
|
|
94
|
+
this.ctx.editor.setText("");
|
|
95
|
+
this.ctx.isPythonMode = false;
|
|
96
|
+
this.ctx.updateEditorBorderColor();
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
if (
|
|
100
|
+
options.maintenance &&
|
|
101
|
+
(this.ctx.session.isCompacting || this.ctx.autoCompactionLoader || this.ctx.autoCompactionEscapeHandler)
|
|
102
|
+
) {
|
|
103
|
+
this.ctx.session.abortCompaction();
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
106
|
+
if (options.maintenance && this.ctx.session.isGeneratingHandoff) {
|
|
107
|
+
this.ctx.session.abortHandoff();
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
if (options.retry) {
|
|
111
|
+
if (this.#isRetryBackoffActive()) {
|
|
112
|
+
if (this.ctx.retryEscapePrimed) {
|
|
113
|
+
this.ctx.session.abortRetry();
|
|
114
|
+
} else {
|
|
115
|
+
this.ctx.retryEscapePrimed = true;
|
|
116
|
+
this.ctx.session.retryNow();
|
|
117
|
+
}
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
120
|
+
this.ctx.retryEscapePrimed = false;
|
|
121
|
+
}
|
|
122
|
+
if (options.streaming && this.ctx.session.isStreaming) {
|
|
123
|
+
if (this.ctx.session.hasQueuedSteering && !this.#steerConsumePending) {
|
|
124
|
+
// First Esc with a queued steer: silently consume it and
|
|
125
|
+
// auto-continue via steer-on-interrupt instead of stalling on
|
|
126
|
+
// "Operation aborted".
|
|
127
|
+
this.#steerConsumePending = true;
|
|
128
|
+
void this.#abortInteractive({ silent: true });
|
|
129
|
+
} else {
|
|
130
|
+
void this.#abortInteractive();
|
|
131
|
+
}
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
#installGlobalInterruptListener(): void {
|
|
138
|
+
if (typeof this.ctx.ui.addInputListener !== "function") {
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
this.#globalInterruptUnsubscribe?.();
|
|
142
|
+
this.#globalInterruptUnsubscribe = this.ctx.ui.addInputListener(data => {
|
|
143
|
+
if (!this.#matchesInterruptKey(data)) {
|
|
144
|
+
return undefined;
|
|
145
|
+
}
|
|
146
|
+
if (this.ctx.hasActiveBtw() && this.ctx.handleBtwEscape()) {
|
|
147
|
+
return { consume: true };
|
|
148
|
+
}
|
|
149
|
+
const hookDialogActive = this.#hasHookDialog();
|
|
150
|
+
if (
|
|
151
|
+
this.#handleCancellableWorkEscape({
|
|
152
|
+
loading: hookDialogActive,
|
|
153
|
+
processes: hookDialogActive,
|
|
154
|
+
modes: false,
|
|
155
|
+
maintenance: true,
|
|
156
|
+
retry: true,
|
|
157
|
+
streaming: hookDialogActive,
|
|
158
|
+
})
|
|
159
|
+
) {
|
|
160
|
+
return { consume: true };
|
|
161
|
+
}
|
|
162
|
+
return undefined;
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
45
166
|
#abortInteractive(options?: { silent?: boolean }): Promise<void> {
|
|
46
167
|
return this.ctx.session.abort({
|
|
47
168
|
timeoutMs: INTERACTIVE_ABORT_CLEANUP_TIMEOUT_MS,
|
|
@@ -60,6 +181,7 @@ export class InputController {
|
|
|
60
181
|
this.ctx.session.isStreaming ||
|
|
61
182
|
this.ctx.session.isCompacting ||
|
|
62
183
|
this.ctx.session.isGeneratingHandoff ||
|
|
184
|
+
this.ctx.session.isRetrying ||
|
|
63
185
|
this.ctx.session.isBashRunning ||
|
|
64
186
|
this.ctx.session.isEvalRunning ||
|
|
65
187
|
this.ctx.autoCompactionLoader ||
|
|
@@ -67,6 +189,8 @@ export class InputController {
|
|
|
67
189
|
this.ctx.autoCompactionEscapeHandler ||
|
|
68
190
|
this.ctx.retryEscapeHandler,
|
|
69
191
|
);
|
|
192
|
+
this.#installGlobalInterruptListener();
|
|
193
|
+
|
|
70
194
|
// An open btw panel must stay dismissable with Esc even while another
|
|
71
195
|
// controller (auto-compaction, auto-retry, manual compaction, etc.) has
|
|
72
196
|
// temporarily replaced editor.onEscape. This priority hook is never
|
|
@@ -87,6 +211,9 @@ export class InputController {
|
|
|
87
211
|
}
|
|
88
212
|
this.#steerConsumePending = false;
|
|
89
213
|
}
|
|
214
|
+
if (this.#handleCancellableWorkEscape({ maintenance: true, retry: true })) {
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
90
217
|
// Normal input state with user-typed text: Esc must not interrupt a
|
|
91
218
|
// running task (streaming turn, bash/eval). A double Esc within the
|
|
92
219
|
// 500ms window clears the composer instead. Bash/Python input modes
|
|
@@ -101,35 +228,19 @@ export class InputController {
|
|
|
101
228
|
}
|
|
102
229
|
return;
|
|
103
230
|
}
|
|
104
|
-
if (
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
this.ctx.session.abortEval();
|
|
118
|
-
} else if (this.ctx.isPythonMode) {
|
|
119
|
-
this.ctx.editor.setText("");
|
|
120
|
-
this.ctx.isPythonMode = false;
|
|
121
|
-
this.ctx.updateEditorBorderColor();
|
|
122
|
-
} else if (this.ctx.session.isStreaming) {
|
|
123
|
-
if (this.ctx.session.hasQueuedSteering && !this.#steerConsumePending) {
|
|
124
|
-
// First Esc with a queued steer: silently consume it and
|
|
125
|
-
// auto-continue via steer-on-interrupt instead of stalling on
|
|
126
|
-
// "Operation aborted".
|
|
127
|
-
this.#steerConsumePending = true;
|
|
128
|
-
void this.#abortInteractive({ silent: true });
|
|
129
|
-
} else {
|
|
130
|
-
void this.#abortInteractive();
|
|
131
|
-
}
|
|
132
|
-
} else if (!this.ctx.editor.getText().trim()) {
|
|
231
|
+
if (
|
|
232
|
+
this.#handleCancellableWorkEscape({
|
|
233
|
+
loading: true,
|
|
234
|
+
processes: true,
|
|
235
|
+
modes: true,
|
|
236
|
+
maintenance: true,
|
|
237
|
+
retry: true,
|
|
238
|
+
streaming: true,
|
|
239
|
+
})
|
|
240
|
+
) {
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
if (!this.ctx.editor.getText().trim()) {
|
|
133
244
|
// Double-interrupt with empty editor triggers /tree, /branch, or nothing based on setting
|
|
134
245
|
const action = settings.get("doubleEscapeAction");
|
|
135
246
|
if (action !== "none") {
|
|
@@ -202,9 +313,18 @@ export class InputController {
|
|
|
202
313
|
this.ctx.editor.onDequeue = () => this.handleDequeue();
|
|
203
314
|
this.ctx.editor.setActionKeys("app.message.queue", this.ctx.keybindings.getKeys("app.message.queue"));
|
|
204
315
|
this.ctx.editor.onQueue = () => void this.handleQueueSubmit();
|
|
316
|
+
this.ctx.editor.onTab = () => {
|
|
317
|
+
if (!this.ctx.session.isStreaming && !this.ctx.session.isCompacting) return false;
|
|
318
|
+
void this.handleQueueSubmit();
|
|
319
|
+
return true;
|
|
320
|
+
};
|
|
205
321
|
this.ctx.editor.onTabDeclined = () => {
|
|
206
322
|
if (this.ctx.session.isStreaming || this.ctx.session.isCompacting) void this.handleQueueSubmit();
|
|
207
323
|
};
|
|
324
|
+
this.ctx.editor.onViewportPageScroll = direction => this.ctx.ui.scrollViewportPages(direction);
|
|
325
|
+
this.ctx.editor.onViewportFollowLive = () => {
|
|
326
|
+
this.ctx.ui.followLiveViewport();
|
|
327
|
+
};
|
|
208
328
|
|
|
209
329
|
this.ctx.editor.clearCustomKeyHandlers();
|
|
210
330
|
// Wire up extension shortcuts
|
|
@@ -524,19 +644,18 @@ export class InputController {
|
|
|
524
644
|
|
|
525
645
|
/**
|
|
526
646
|
* Dispatch skill slash invocation(s) (`/skill:<name>`) through custom messages
|
|
527
|
-
* using the supplied `streamingBehavior`. Returns true if the text
|
|
528
|
-
* recognised skill command chain and was dispatched. A
|
|
529
|
-
* file is surfaced via `showError` but still returns
|
|
530
|
-
* already cleared on the success path, so falling
|
|
531
|
-
* handling at that point would double-submit. Returns
|
|
532
|
-
*
|
|
533
|
-
*
|
|
647
|
+
* using the supplied `streamingBehavior`. Returns true if the text contains a
|
|
648
|
+
* recognised canonical skill command or command chain and was dispatched. A
|
|
649
|
+
* failure to load a skill file is surfaced via `showError` but still returns
|
|
650
|
+
* true — the editor was already cleared on the success path, so falling
|
|
651
|
+
* through to plain-text handling at that point would double-submit. Returns
|
|
652
|
+
* false when the text has no registered canonical skill invocation, so the
|
|
653
|
+
* caller can fall through to plain-text handling (this branch
|
|
534
654
|
* leaves the editor state untouched). `streamingBehavior` is only consulted
|
|
535
655
|
* while the agent is streaming; the idle path of `promptCustomMessage`
|
|
536
656
|
* ignores it.
|
|
537
657
|
*/
|
|
538
658
|
async #invokeSkillCommand(text: string, streamingBehavior: "steer" | "followUp"): Promise<boolean> {
|
|
539
|
-
if (!text.startsWith("/")) return false;
|
|
540
659
|
const invocations = parseSkillInvocations(text, this.ctx.skillCommands ?? new Map());
|
|
541
660
|
if (invocations.length === 0) return false;
|
|
542
661
|
this.ctx.editor.addToHistory(text);
|
|
@@ -727,6 +846,7 @@ export class InputController {
|
|
|
727
846
|
this.ctx.editor.onEscape = this.ctx.retryEscapeHandler;
|
|
728
847
|
this.ctx.retryEscapeHandler = undefined;
|
|
729
848
|
}
|
|
849
|
+
this.ctx.retryEscapePrimed = false;
|
|
730
850
|
this.ctx.statusContainer.clear();
|
|
731
851
|
this.ctx.statusLine.dispose();
|
|
732
852
|
|