@gajae-code/coding-agent 0.7.9 → 0.7.11
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 +2 -0
- package/dist/types/config/keybindings.d.ts +11 -5
- package/dist/types/config/settings-schema.d.ts +10 -1
- package/dist/types/defaults/gjc-defaults.d.ts +7 -0
- package/dist/types/extensibility/extensions/types.d.ts +11 -0
- package/dist/types/gjc-runtime/launch-tmux.d.ts +10 -0
- package/dist/types/gjc-runtime/ralplan-runtime.d.ts +3 -2
- package/dist/types/gjc-runtime/restricted-role-agent-bash.d.ts +1 -0
- package/dist/types/gjc-runtime/state-writer.d.ts +1 -0
- package/dist/types/gjc-runtime/ultragoal-guard.d.ts +4 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +0 -4
- package/dist/types/modes/components/custom-editor.d.ts +2 -1
- package/dist/types/modes/components/hook-selector.d.ts +5 -0
- package/dist/types/modes/components/skill-message.d.ts +1 -0
- package/dist/types/modes/controllers/command-controller.d.ts +1 -0
- package/dist/types/modes/controllers/input-controller.d.ts +4 -0
- package/dist/types/modes/interactive-mode.d.ts +1 -0
- package/dist/types/modes/prompt-action-autocomplete.d.ts +2 -0
- package/dist/types/modes/shared/agent-wire/deep-interview-gate.d.ts +20 -6
- package/dist/types/modes/slash-command-visibility.d.ts +1 -0
- package/dist/types/modes/types.d.ts +1 -0
- package/dist/types/notifications/html-format.d.ts +2 -0
- package/dist/types/notifications/recent-activity.d.ts +5 -1
- package/dist/types/notifications/telegram-daemon-control.d.ts +8 -0
- package/dist/types/notifications/telegram-reference.d.ts +4 -0
- package/dist/types/notifications/threaded-render.d.ts +1 -0
- package/dist/types/sdk.d.ts +1 -1
- package/dist/types/session/agent-session.d.ts +8 -5
- package/dist/types/slash-commands/types.d.ts +2 -0
- package/dist/types/tools/ask.d.ts +2 -0
- package/dist/types/utils/cmux-workspace.d.ts +52 -0
- package/dist/types/web/search/index.d.ts +4 -2
- package/dist/types/web/search/provider.d.ts +14 -0
- package/dist/types/web/search/providers/utils.d.ts +42 -13
- package/dist/types/web/search/render.d.ts +1 -0
- package/package.json +7 -7
- package/src/cli/update-cli.ts +24 -0
- package/src/cli/web-search-cli.ts +5 -7
- package/src/cli.ts +1 -1
- package/src/commands/launch.ts +1 -1
- package/src/commands/ralplan.ts +1 -0
- package/src/config/keybindings.ts +14 -5
- package/src/config/model-profiles.ts +16 -0
- package/src/config/settings-schema.ts +11 -1
- package/src/coordinator-mcp/server.ts +9 -3
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +26 -2
- package/src/defaults/gjc/skills/ralplan/SKILL.md +16 -14
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +37 -112
- package/src/defaults/gjc-defaults.ts +16 -0
- package/src/extensibility/extensions/runner.ts +2 -1
- package/src/extensibility/extensions/types.ts +11 -0
- package/src/gjc-runtime/launch-tmux.ts +209 -26
- package/src/gjc-runtime/ralplan-runtime.ts +22 -6
- package/src/gjc-runtime/restricted-role-agent-bash.ts +1 -0
- package/src/gjc-runtime/state-runtime.ts +8 -5
- package/src/gjc-runtime/state-writer.ts +5 -5
- package/src/gjc-runtime/ultragoal-guard.ts +139 -19
- package/src/gjc-runtime/ultragoal-runtime.ts +16 -124
- package/src/gjc-runtime/workflow-manifest.generated.json +0 -8
- package/src/gjc-runtime/workflow-manifest.ts +0 -1
- package/src/hooks/skill-state.ts +33 -73
- package/src/internal-urls/docs-index.generated.ts +6 -4
- package/src/main.ts +2 -2
- package/src/migrate/executor.ts +23 -6
- package/src/modes/components/custom-editor.ts +8 -0
- package/src/modes/components/hook-selector.ts +21 -3
- package/src/modes/components/model-selector.ts +6 -2
- package/src/modes/components/settings-selector.ts +47 -5
- package/src/modes/components/skill-message.ts +18 -10
- package/src/modes/components/status-line/presets.ts +11 -0
- package/src/modes/controllers/command-controller.ts +23 -0
- package/src/modes/controllers/extension-ui-controller.ts +18 -2
- package/src/modes/controllers/input-controller.ts +41 -9
- package/src/modes/interactive-mode.ts +16 -7
- package/src/modes/prompt-action-autocomplete.ts +23 -1
- package/src/modes/shared/agent-wire/deep-interview-gate.ts +53 -9
- package/src/modes/slash-command-visibility.ts +9 -0
- package/src/modes/types.ts +1 -0
- package/src/modes/utils/hotkeys-markdown.ts +5 -2
- package/src/modes/utils/ui-helpers.ts +76 -3
- package/src/notifications/html-format.ts +75 -1
- package/src/notifications/index.ts +12 -1
- package/src/notifications/recent-activity.ts +32 -8
- package/src/notifications/telegram-daemon-control.ts +9 -0
- package/src/notifications/telegram-daemon.ts +68 -25
- package/src/notifications/telegram-reference.ts +36 -14
- package/src/notifications/threaded-render.ts +14 -6
- package/src/prompts/agents/architect.md +7 -4
- package/src/prompts/agents/critic.md +15 -6
- package/src/prompts/agents/planner.md +8 -3
- package/src/prompts/system/system-prompt.md +5 -1
- package/src/prompts/tools/bash.md +1 -1
- package/src/sdk.ts +6 -11
- package/src/session/agent-session.ts +60 -17
- package/src/slash-commands/builtin-registry.ts +30 -10
- package/src/slash-commands/types.ts +2 -0
- package/src/tools/ask.ts +136 -16
- package/src/tools/bash.ts +24 -8
- package/src/utils/cmux-workspace.ts +233 -0
- package/src/utils/title-generator.ts +2 -0
- package/src/web/kagi.ts +1 -1
- package/src/web/parallel.ts +2 -2
- package/src/web/search/index.ts +137 -52
- package/src/web/search/provider.ts +80 -0
- package/src/web/search/providers/anthropic.ts +1 -1
- package/src/web/search/providers/brave.ts +1 -1
- package/src/web/search/providers/codex.ts +1 -1
- package/src/web/search/providers/duckduckgo.ts +1 -1
- package/src/web/search/providers/exa.ts +1 -1
- package/src/web/search/providers/gemini.ts +7 -3
- package/src/web/search/providers/jina.ts +1 -1
- package/src/web/search/providers/kimi.ts +6 -1
- package/src/web/search/providers/openai-compatible.ts +1 -1
- package/src/web/search/providers/parallel.ts +1 -1
- package/src/web/search/providers/perplexity.ts +2 -2
- package/src/web/search/providers/searxng.ts +1 -1
- package/src/web/search/providers/synthetic.ts +1 -1
- package/src/web/search/providers/tavily.ts +1 -1
- package/src/web/search/providers/utils.ts +67 -17
- package/src/web/search/providers/xai.ts +1 -1
- package/src/web/search/providers/zai.ts +1 -1
- package/src/web/search/render.ts +3 -0
|
@@ -29,6 +29,8 @@ interface PromptActionAutocompleteOptions {
|
|
|
29
29
|
copyCurrentLine: () => void;
|
|
30
30
|
copyPrompt: () => void;
|
|
31
31
|
pasteImage: () => void;
|
|
32
|
+
newSession: () => void;
|
|
33
|
+
showHelp: () => void;
|
|
32
34
|
scrollTmuxToPreviousUserInput: () => void;
|
|
33
35
|
undo: (prefix: string) => void;
|
|
34
36
|
moveCursorToMessageEnd: () => void;
|
|
@@ -101,6 +103,12 @@ function mergeAutocompleteSuggestions(
|
|
|
101
103
|
|
|
102
104
|
return { items, prefix: primary.prefix };
|
|
103
105
|
}
|
|
106
|
+
const ADVANCED_SLASH_COMMAND_PRIORITIES = new Map<string, number>([["grok-build-usage", -100]]);
|
|
107
|
+
|
|
108
|
+
function getSlashCommandPriority(command: SlashCommand | undefined, item: AutocompleteItem): number {
|
|
109
|
+
if (command?.priority !== undefined) return command.priority;
|
|
110
|
+
return ADVANCED_SLASH_COMMAND_PRIORITIES.get(item.value) ?? 0;
|
|
111
|
+
}
|
|
104
112
|
|
|
105
113
|
function sortSlashCommandSuggestions(
|
|
106
114
|
suggestions: { items: AutocompleteItem[]; prefix: string } | null,
|
|
@@ -123,7 +131,7 @@ function sortSlashCommandSuggestions(
|
|
|
123
131
|
index,
|
|
124
132
|
commandIndex,
|
|
125
133
|
matchRank: getSlashCommandMatchRank(query, lowerName),
|
|
126
|
-
priority: command
|
|
134
|
+
priority: getSlashCommandPriority(command, item),
|
|
127
135
|
score: Math.max(nameScore, descScore),
|
|
128
136
|
};
|
|
129
137
|
})
|
|
@@ -358,6 +366,20 @@ export function createPromptActionAutocompleteProvider(
|
|
|
358
366
|
): PromptActionAutocompleteProvider {
|
|
359
367
|
const editorKeybindings = getKeybindings();
|
|
360
368
|
const actions: PromptActionDefinition[] = [
|
|
369
|
+
{
|
|
370
|
+
id: "new-session",
|
|
371
|
+
label: "Start new session",
|
|
372
|
+
description: formatKeyHints(options.keybindings.getKeys("app.session.new")) || "/new",
|
|
373
|
+
keywords: ["new", "session", "fresh", "clear", "start", "conversation"],
|
|
374
|
+
execute: options.newSession,
|
|
375
|
+
},
|
|
376
|
+
{
|
|
377
|
+
id: "help",
|
|
378
|
+
label: "Open command help",
|
|
379
|
+
description: "/help",
|
|
380
|
+
keywords: ["help", "commands", "command", "palette", "shortcuts", "beginner"],
|
|
381
|
+
execute: options.showHelp,
|
|
382
|
+
},
|
|
361
383
|
{
|
|
362
384
|
id: "copy-line",
|
|
363
385
|
label: "Copy current line",
|
|
@@ -53,6 +53,7 @@ export interface AskGateResult {
|
|
|
53
53
|
multi: boolean;
|
|
54
54
|
selectedOptions: string[];
|
|
55
55
|
customInput?: string;
|
|
56
|
+
clarificationQuestion?: string;
|
|
56
57
|
}
|
|
57
58
|
|
|
58
59
|
/**
|
|
@@ -61,12 +62,15 @@ export interface AskGateResult {
|
|
|
61
62
|
* `selected` are picked option labels; free text is conveyed by `other: true`
|
|
62
63
|
* plus `custom`, encoded separately from `selected` so a real option whose label
|
|
63
64
|
* happens to equal the display sentinel can never collide with the free-text path.
|
|
65
|
+
*
|
|
66
|
+
* `action: "clarify"` is a non-answer. It lets a Deep Interview user ask about
|
|
67
|
+
* the presented choices without advancing the round, scoring ambiguity, or
|
|
68
|
+
* recording selection state.
|
|
64
69
|
*/
|
|
65
|
-
export
|
|
66
|
-
selected: string[];
|
|
67
|
-
other?:
|
|
68
|
-
|
|
69
|
-
}
|
|
70
|
+
export type DeepInterviewGateAnswer =
|
|
71
|
+
| { selected: string[]; other?: false; custom?: undefined; action?: "answer" }
|
|
72
|
+
| { selected: string[]; other: true; custom: string; action?: "answer" }
|
|
73
|
+
| { action: "clarify"; question: string };
|
|
70
74
|
|
|
71
75
|
export class DeepInterviewGateError extends Error {
|
|
72
76
|
constructor(
|
|
@@ -75,6 +79,7 @@ export class DeepInterviewGateError extends Error {
|
|
|
75
79
|
| "unknown_option"
|
|
76
80
|
| "multi_not_allowed"
|
|
77
81
|
| "missing_custom"
|
|
82
|
+
| "missing_clarification"
|
|
78
83
|
| "empty_selection"
|
|
79
84
|
| "duplicate_selection",
|
|
80
85
|
message: string,
|
|
@@ -127,13 +132,22 @@ function questionAnswerSchema(question: AskGateQuestion, labels: string[]): RpcJ
|
|
|
127
132
|
selected: selectedBase,
|
|
128
133
|
other: { type: "boolean", description: "set true to provide a free-text answer in `custom`" },
|
|
129
134
|
custom: { type: "string", minLength: 1, description: "free-text answer; required when `other` is true" },
|
|
135
|
+
action: {
|
|
136
|
+
type: "string",
|
|
137
|
+
enum: ["answer", "clarify"],
|
|
138
|
+
description: "set to `clarify` to ask about the choices without answering the round",
|
|
139
|
+
},
|
|
140
|
+
question: {
|
|
141
|
+
type: "string",
|
|
142
|
+
minLength: 1,
|
|
143
|
+
description: "clarification question; required when action is `clarify`",
|
|
144
|
+
},
|
|
130
145
|
},
|
|
131
|
-
required: ["selected"],
|
|
132
146
|
additionalProperties: false,
|
|
133
147
|
anyOf: [
|
|
134
148
|
{
|
|
135
149
|
type: "object",
|
|
136
|
-
properties: { selected: selectedOnly, other: { const: false } },
|
|
150
|
+
properties: { selected: selectedOnly, other: { const: false }, action: { const: "answer" } },
|
|
137
151
|
required: ["selected"],
|
|
138
152
|
additionalProperties: false,
|
|
139
153
|
},
|
|
@@ -143,10 +157,20 @@ function questionAnswerSchema(question: AskGateQuestion, labels: string[]): RpcJ
|
|
|
143
157
|
selected: selectedWithOther,
|
|
144
158
|
other: { const: true },
|
|
145
159
|
custom: { type: "string", minLength: 1 },
|
|
160
|
+
action: { const: "answer" },
|
|
146
161
|
},
|
|
147
162
|
required: ["selected", "other", "custom"],
|
|
148
163
|
additionalProperties: false,
|
|
149
164
|
},
|
|
165
|
+
{
|
|
166
|
+
type: "object",
|
|
167
|
+
properties: {
|
|
168
|
+
action: { const: "clarify" },
|
|
169
|
+
question: { type: "string", minLength: 1 },
|
|
170
|
+
},
|
|
171
|
+
required: ["action", "question"],
|
|
172
|
+
additionalProperties: false,
|
|
173
|
+
},
|
|
150
174
|
],
|
|
151
175
|
};
|
|
152
176
|
}
|
|
@@ -185,6 +209,7 @@ export function questionToGate(question: AskGateQuestion): OpenGateInput {
|
|
|
185
209
|
multi: question.multi ?? false,
|
|
186
210
|
options: labels,
|
|
187
211
|
other_option: GATE_OTHER_OPTION,
|
|
212
|
+
clarification_action: "clarify",
|
|
188
213
|
...(question.deepInterview
|
|
189
214
|
? structuredDeepInterviewState(question.deepInterview)
|
|
190
215
|
: deepInterviewQuestionState(question.question)),
|
|
@@ -195,8 +220,14 @@ export function questionToGate(question: AskGateQuestion): OpenGateInput {
|
|
|
195
220
|
|
|
196
221
|
function isAnswer(value: unknown): value is DeepInterviewGateAnswer {
|
|
197
222
|
if (typeof value !== "object" || value === null) return false;
|
|
198
|
-
const v = value as
|
|
223
|
+
const v = value as Record<string, unknown>;
|
|
224
|
+
if (v.action === "clarify") {
|
|
225
|
+
return (
|
|
226
|
+
typeof v.question === "string" && v.selected === undefined && v.other === undefined && v.custom === undefined
|
|
227
|
+
);
|
|
228
|
+
}
|
|
199
229
|
return (
|
|
230
|
+
(v.action === undefined || v.action === "answer") &&
|
|
200
231
|
Array.isArray(v.selected) &&
|
|
201
232
|
v.selected.every(s => typeof s === "string") &&
|
|
202
233
|
(v.other === undefined || typeof v.other === "boolean") &&
|
|
@@ -214,11 +245,24 @@ export function gateAnswerToResult(question: AskGateQuestion, answer: unknown):
|
|
|
214
245
|
if (!isAnswer(answer)) {
|
|
215
246
|
throw new DeepInterviewGateError(
|
|
216
247
|
"invalid_answer_shape",
|
|
217
|
-
|
|
248
|
+
'answer must be an answer ({ selected: string[]; other?: boolean; custom?: string }) or clarification ({ action: "clarify"; question: string })',
|
|
218
249
|
);
|
|
219
250
|
}
|
|
220
251
|
const labels = question.options.map(o => o.label);
|
|
221
252
|
const multi = question.multi ?? false;
|
|
253
|
+
if (answer.action === "clarify") {
|
|
254
|
+
if (answer.question.trim() === "") {
|
|
255
|
+
throw new DeepInterviewGateError("missing_clarification", "clarification question is required");
|
|
256
|
+
}
|
|
257
|
+
return {
|
|
258
|
+
id: question.id,
|
|
259
|
+
question: question.question,
|
|
260
|
+
options: labels,
|
|
261
|
+
multi,
|
|
262
|
+
selectedOptions: [],
|
|
263
|
+
clarificationQuestion: answer.question,
|
|
264
|
+
};
|
|
265
|
+
}
|
|
222
266
|
const valid = new Set(labels);
|
|
223
267
|
for (const sel of answer.selected) {
|
|
224
268
|
if (!valid.has(sel)) throw new DeepInterviewGateError("unknown_option", `unknown option: ${sel}`);
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
const BEGINNER_HIDDEN_EXTENSION_COMMANDS = new Set(["grok-build-usage"]);
|
|
2
|
+
|
|
3
|
+
export function shouldShowExtensionCommand(name: string, activeProvider: string | undefined): boolean {
|
|
4
|
+
if (!BEGINNER_HIDDEN_EXTENSION_COMMANDS.has(name)) return true;
|
|
5
|
+
if (name === "grok-build-usage") {
|
|
6
|
+
return activeProvider === "grok-build" || Boolean(process.env.GROK_CLI_OAUTH_TOKEN);
|
|
7
|
+
}
|
|
8
|
+
return true;
|
|
9
|
+
}
|
package/src/modes/types.ts
CHANGED
|
@@ -209,6 +209,7 @@ export interface InteractiveModeContext {
|
|
|
209
209
|
handleUsageCommand(reports?: UsageReport[] | null): Promise<void>;
|
|
210
210
|
handleChangelogCommand(showFull?: boolean): Promise<void>;
|
|
211
211
|
handleHotkeysCommand(): void;
|
|
212
|
+
handleHelpCommand(): void;
|
|
212
213
|
handleToolsCommand(): void;
|
|
213
214
|
handleContextCommand(): void;
|
|
214
215
|
handleDumpCommand(): void;
|
|
@@ -38,6 +38,8 @@ export function buildHotkeysMarkdown(bindings: HotkeysMarkdownBindings): string
|
|
|
38
38
|
`| \`${appKey(bindings, "app.clear")}\` | Clear editor (first) / exit (second) |`,
|
|
39
39
|
`| \`${appKey(bindings, "app.exit")}\` | Exit (when editor is empty) |`,
|
|
40
40
|
`| \`${appKey(bindings, "app.suspend")}\` | Suspend to background |`,
|
|
41
|
+
`| \`${appKey(bindings, "app.commandPalette.open")}\` | Open command palette |`,
|
|
42
|
+
`| \`${appKey(bindings, "app.session.new")}\` | Start a fresh session |`,
|
|
41
43
|
`| \`${appKey(bindings, "app.thinking.cycle")}\` | Cycle thinking level |`,
|
|
42
44
|
`| \`${appKey(bindings, "app.model.cycleForward")}\` | Cycle configured model roles |`,
|
|
43
45
|
`| \`${appKey(bindings, "app.model.cycleBackward")}\` | Cycle configured model roles temporarily |`,
|
|
@@ -51,8 +53,9 @@ export function buildHotkeysMarkdown(bindings: HotkeysMarkdownBindings): string
|
|
|
51
53
|
`| \`${appKey(bindings, "app.editor.external")}\` | Edit message in external editor |`,
|
|
52
54
|
`| \`${appKey(bindings, "app.clipboard.pasteImage")}\` | Paste image from clipboard |`,
|
|
53
55
|
`| \`${appKey(bindings, "app.stt.toggle")}\` | Toggle speech-to-text recording |`,
|
|
54
|
-
"|
|
|
55
|
-
"|
|
|
56
|
+
"| `?` | Show help / shortcuts |",
|
|
57
|
+
"| `#` | Prompt actions (command-palette style actions) |",
|
|
58
|
+
"| `/` | Slash commands (try `/help` or `/new`) |",
|
|
56
59
|
"| `!` | Run bash command |",
|
|
57
60
|
"| `!!` | Run bash command (excluded from context) |",
|
|
58
61
|
"| `$` | Run Python in shared kernel |",
|
|
@@ -2,6 +2,8 @@ import type { AgentMessage } from "@gajae-code/agent-core";
|
|
|
2
2
|
import type { AssistantMessage, ImageContent, Message } from "@gajae-code/ai";
|
|
3
3
|
import { type Component, Spacer, Text, TruncatedText } from "@gajae-code/tui";
|
|
4
4
|
import { settings } from "../../config/settings";
|
|
5
|
+
import { resolveSubskillActivationForSkillInvocation } from "../../extensibility/gjc-plugins";
|
|
6
|
+
import { buildSkillPromptMessage, parseSkillInvocations } from "../../extensibility/skills";
|
|
5
7
|
import { AssistantMessageComponent } from "../../modes/components/assistant-message";
|
|
6
8
|
import { BashExecutionComponent } from "../../modes/components/bash-execution";
|
|
7
9
|
import { BranchSummaryMessageComponent } from "../../modes/components/branch-summary-message";
|
|
@@ -610,9 +612,80 @@ export class UiHelpers {
|
|
|
610
612
|
this.ctx.updatePendingMessagesDisplay();
|
|
611
613
|
this.ctx.showStatus("Queued message for after compaction");
|
|
612
614
|
}
|
|
615
|
+
#hasSkillInvocations(text: string): boolean {
|
|
616
|
+
return parseSkillInvocations(text, this.ctx.skillCommands ?? new Map()).length > 0;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
#isCompactionCommandMessage(text: string): boolean {
|
|
620
|
+
return this.#hasSkillInvocations(text) || this.isKnownSlashCommand(text);
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
async #deliverQueuedSkillMessage(message: CompactionQueuedMessage): Promise<boolean> {
|
|
624
|
+
const invocations = parseSkillInvocations(message.text, this.ctx.skillCommands ?? new Map());
|
|
625
|
+
if (invocations.length === 0) {
|
|
626
|
+
return false;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
for (let index = 0; index < invocations.length; index += 1) {
|
|
630
|
+
const invocation = invocations[index];
|
|
631
|
+
if (!invocation) continue;
|
|
632
|
+
|
|
633
|
+
const activationResult = await resolveSubskillActivationForSkillInvocation({
|
|
634
|
+
cwd: this.ctx.sessionManager.getCwd(),
|
|
635
|
+
sessionId: this.ctx.session.sessionId,
|
|
636
|
+
skillName: invocation.skill.name,
|
|
637
|
+
args: invocation.args,
|
|
638
|
+
});
|
|
639
|
+
const built = await buildSkillPromptMessage(invocation.skill, activationResult.cleanedArgs, {
|
|
640
|
+
subskillActivation: activationResult.activation,
|
|
641
|
+
subskillActivationSet: activationResult.activeSubskillsToPersist,
|
|
642
|
+
cwd: this.ctx.sessionManager.getCwd(),
|
|
643
|
+
sessionId: this.ctx.session.sessionId,
|
|
644
|
+
});
|
|
645
|
+
const details: SkillPromptDetails = built.details;
|
|
646
|
+
const displayText = `/${invocation.commandName}${activationResult.cleanedArgs ? ` ${activationResult.cleanedArgs}` : ""}`;
|
|
647
|
+
|
|
648
|
+
if (this.ctx.session.isStreaming) {
|
|
649
|
+
const tag = this.ctx.session.enqueueCustomMessageDisplay(displayText, message.mode);
|
|
650
|
+
details.__pendingDisplayTag = tag;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
const isLast = index === invocations.length - 1;
|
|
654
|
+
if (!this.ctx.session.isStreaming && !isLast) {
|
|
655
|
+
await this.ctx.session.sendCustomMessage({
|
|
656
|
+
customType: SKILL_PROMPT_MESSAGE_TYPE,
|
|
657
|
+
content: built.message,
|
|
658
|
+
display: true,
|
|
659
|
+
details,
|
|
660
|
+
attribution: "user",
|
|
661
|
+
});
|
|
662
|
+
continue;
|
|
663
|
+
}
|
|
613
664
|
|
|
665
|
+
await this.ctx.session.promptCustomMessage(
|
|
666
|
+
{
|
|
667
|
+
customType: SKILL_PROMPT_MESSAGE_TYPE,
|
|
668
|
+
content: built.message,
|
|
669
|
+
display: true,
|
|
670
|
+
details,
|
|
671
|
+
attribution: "user",
|
|
672
|
+
},
|
|
673
|
+
{ streamingBehavior: message.mode },
|
|
674
|
+
);
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
if (this.ctx.session.isStreaming) {
|
|
678
|
+
this.ctx.updatePendingMessagesDisplay();
|
|
679
|
+
this.ctx.ui.requestRender();
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
return true;
|
|
683
|
+
}
|
|
614
684
|
async #deliverQueuedMessage(message: CompactionQueuedMessage): Promise<void> {
|
|
615
|
-
if (this
|
|
685
|
+
if (await this.#deliverQueuedSkillMessage(message)) {
|
|
686
|
+
return;
|
|
687
|
+
}
|
|
688
|
+
if (this.isKnownSlashCommand(message.text)) {
|
|
616
689
|
await this.ctx.session.prompt(message.text);
|
|
617
690
|
return;
|
|
618
691
|
}
|
|
@@ -671,14 +744,14 @@ export class UiHelpers {
|
|
|
671
744
|
|
|
672
745
|
let firstPromptIndex = -1;
|
|
673
746
|
for (let i = 0; i < queuedMessages.length; i++) {
|
|
674
|
-
if (!this
|
|
747
|
+
if (!this.#isCompactionCommandMessage(queuedMessages[i].text)) {
|
|
675
748
|
firstPromptIndex = i;
|
|
676
749
|
break;
|
|
677
750
|
}
|
|
678
751
|
}
|
|
679
752
|
if (firstPromptIndex === -1) {
|
|
680
753
|
for (const message of queuedMessages) {
|
|
681
|
-
await this
|
|
754
|
+
await this.#deliverQueuedMessage(message);
|
|
682
755
|
}
|
|
683
756
|
return;
|
|
684
757
|
}
|
|
@@ -248,6 +248,8 @@ interface Token {
|
|
|
248
248
|
value: string;
|
|
249
249
|
/** Tag name if this token opens a tag, else undefined. */
|
|
250
250
|
open?: string;
|
|
251
|
+
/** Exact opening tag text, including attributes, for safe chunk reopening. */
|
|
252
|
+
openTag?: string;
|
|
251
253
|
/** Tag name if this token closes a tag, else undefined. */
|
|
252
254
|
close?: string;
|
|
253
255
|
}
|
|
@@ -266,8 +268,10 @@ function tokenize(html: string): Token[] {
|
|
|
266
268
|
const openMatch = /^<([a-z-]+)(?:\s[^>]*)?>$/i.exec(raw);
|
|
267
269
|
const token: Token = { value: raw };
|
|
268
270
|
if (close && ALLOWED_TAGS.has(close[1]!.toLowerCase())) token.close = close[1]!.toLowerCase();
|
|
269
|
-
else if (openMatch && ALLOWED_TAGS.has(openMatch[1]!.toLowerCase()))
|
|
271
|
+
else if (openMatch && ALLOWED_TAGS.has(openMatch[1]!.toLowerCase())) {
|
|
270
272
|
token.open = openMatch[1]!.toLowerCase();
|
|
273
|
+
token.openTag = raw;
|
|
274
|
+
}
|
|
271
275
|
tokens.push(token);
|
|
272
276
|
i = end + 1;
|
|
273
277
|
continue;
|
|
@@ -330,6 +334,76 @@ export function truncateTelegramHtml(message: string, max = TELEGRAM_MESSAGE_LIM
|
|
|
330
334
|
return out + closersFor(stack) + effectiveMarker;
|
|
331
335
|
}
|
|
332
336
|
|
|
337
|
+
interface OpenTag {
|
|
338
|
+
name: string;
|
|
339
|
+
tag: string;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/** Split a finished Telegram HTML message into ordered chunks without splitting tags or entities. */
|
|
343
|
+
export function splitTelegramHtml(message: string, max = TELEGRAM_MESSAGE_LIMIT): string[] {
|
|
344
|
+
if (message.length <= max) return [message];
|
|
345
|
+
const chunks: string[] = [];
|
|
346
|
+
const tokens = tokenize(message);
|
|
347
|
+
const stack: OpenTag[] = [];
|
|
348
|
+
let out = "";
|
|
349
|
+
let chunkHasBody = false;
|
|
350
|
+
|
|
351
|
+
const closersFor = (s: OpenTag[]): string =>
|
|
352
|
+
s
|
|
353
|
+
.map(t => `</${t.name}>`)
|
|
354
|
+
.reverse()
|
|
355
|
+
.join("");
|
|
356
|
+
const openersFor = (s: OpenTag[]): string => s.map(t => t.tag).join("");
|
|
357
|
+
const minimumChunkLengthFor = (s: OpenTag[]): number => openersFor(s).length + closersFor(s).length + 1;
|
|
358
|
+
const flush = (): void => {
|
|
359
|
+
if (!chunkHasBody) return;
|
|
360
|
+
chunks.push(out + closersFor(stack));
|
|
361
|
+
out = openersFor(stack);
|
|
362
|
+
chunkHasBody = false;
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
const updateStackForClose = (name: string): boolean => {
|
|
366
|
+
const idx = stack.findLastIndex(t => t.name === name);
|
|
367
|
+
if (idx === -1) return false;
|
|
368
|
+
stack.splice(idx, 1);
|
|
369
|
+
return true;
|
|
370
|
+
};
|
|
371
|
+
|
|
372
|
+
for (const token of tokens) {
|
|
373
|
+
if (token.open) {
|
|
374
|
+
const nextStack = [...stack, { name: token.open, tag: token.openTag ?? token.value }];
|
|
375
|
+
if (minimumChunkLengthFor(nextStack) > max) continue;
|
|
376
|
+
if (chunkHasBody && out.length + token.value.length + closersFor(nextStack).length > max) flush();
|
|
377
|
+
if (out.length + token.value.length + closersFor(nextStack).length > max) continue;
|
|
378
|
+
out += token.value;
|
|
379
|
+
chunkHasBody = true;
|
|
380
|
+
stack.push({ name: token.open, tag: token.openTag ?? token.value });
|
|
381
|
+
continue;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
if (token.close) {
|
|
385
|
+
const idx = stack.findLastIndex(t => t.name === token.close);
|
|
386
|
+
if (idx === -1) continue;
|
|
387
|
+
const nextStack = stack.toSpliced(idx, 1);
|
|
388
|
+
if (chunkHasBody && out.length + token.value.length + closersFor(nextStack).length > max) flush();
|
|
389
|
+
if (out.length + token.value.length + closersFor(nextStack).length > max) {
|
|
390
|
+
updateStackForClose(token.close);
|
|
391
|
+
continue;
|
|
392
|
+
}
|
|
393
|
+
out += token.value;
|
|
394
|
+
chunkHasBody = true;
|
|
395
|
+
updateStackForClose(token.close);
|
|
396
|
+
continue;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
if (chunkHasBody && out.length + token.value.length + closersFor(stack).length > max) flush();
|
|
400
|
+
out += token.value;
|
|
401
|
+
chunkHasBody = true;
|
|
402
|
+
}
|
|
403
|
+
if (chunkHasBody) chunks.push(out + closersFor(stack));
|
|
404
|
+
return chunks;
|
|
405
|
+
}
|
|
406
|
+
|
|
333
407
|
/** Finalize an optional message: undefined passthrough, else safe-truncate. */
|
|
334
408
|
export function finalizeTelegramHtml(message?: string): string | undefined {
|
|
335
409
|
if (message === undefined) return undefined;
|
|
@@ -297,6 +297,15 @@ function buildIdentity(
|
|
|
297
297
|
return { repo, branch, machine: os.hostname(), title: sessionName };
|
|
298
298
|
}
|
|
299
299
|
|
|
300
|
+
/** Compact cwd label for remote session identity; never emits the full host path by default. */
|
|
301
|
+
function compactCwd(cwd: string): string | undefined {
|
|
302
|
+
const home = os.homedir();
|
|
303
|
+
const resolved = path.resolve(cwd);
|
|
304
|
+
if (resolved === home) return "~";
|
|
305
|
+
const base = path.basename(resolved);
|
|
306
|
+
return base || path.parse(resolved).root || undefined;
|
|
307
|
+
}
|
|
308
|
+
|
|
300
309
|
const execFileAsync = promisify(execFile);
|
|
301
310
|
|
|
302
311
|
/** Best-effort working-tree diff stat for the context update (no throw). */
|
|
@@ -973,7 +982,8 @@ export const createNotificationsExtension: ExtensionFactory = api => {
|
|
|
973
982
|
const tokenUsage = usage && usage.tokens != null ? `${usage.tokens}/${usage.contextWindow}` : undefined;
|
|
974
983
|
const modelId = model?.id;
|
|
975
984
|
void readGitDiffStat(ctx.cwd).then(diff => {
|
|
976
|
-
|
|
985
|
+
const cwd = compactCwd(ctx.cwd);
|
|
986
|
+
if (!diff && !tokenUsage && !modelId && !cwd) return;
|
|
977
987
|
try {
|
|
978
988
|
rt.server.pushFrame(
|
|
979
989
|
JSON.stringify({
|
|
@@ -982,6 +992,7 @@ export const createNotificationsExtension: ExtensionFactory = api => {
|
|
|
982
992
|
tokenUsage,
|
|
983
993
|
model: modelId,
|
|
984
994
|
diff,
|
|
995
|
+
cwd,
|
|
985
996
|
}),
|
|
986
997
|
);
|
|
987
998
|
} catch (e) {
|
|
@@ -25,6 +25,8 @@ export interface RecentSessionEntry {
|
|
|
25
25
|
mtimeMs: number;
|
|
26
26
|
/** True when a terminal breadcrumb points at this session file. */
|
|
27
27
|
currentTerminal?: boolean;
|
|
28
|
+
/** True when this history is an internal helper/sub-agent session. */
|
|
29
|
+
internal?: boolean;
|
|
28
30
|
}
|
|
29
31
|
|
|
30
32
|
export interface RecentActivityDeps {
|
|
@@ -34,17 +36,18 @@ export interface RecentActivityDeps {
|
|
|
34
36
|
breadcrumbPaths?: string[];
|
|
35
37
|
/** Max entries to return (default 20). */
|
|
36
38
|
limit?: number;
|
|
39
|
+
/** Include internal helper/sub-agent sessions (default true). */
|
|
40
|
+
includeInternal?: boolean;
|
|
37
41
|
/** Injection seam for tests. */
|
|
38
|
-
|
|
42
|
+
readInitialLines?: (file: string, maxLines: number) => string[];
|
|
39
43
|
}
|
|
40
44
|
|
|
41
|
-
function
|
|
45
|
+
function defaultReadInitialLines(file: string, maxLines: number): string[] {
|
|
42
46
|
try {
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
return nl === -1 ? buf : buf.slice(0, nl);
|
|
47
|
+
const lines = fs.readFileSync(file, "utf8").split("\n");
|
|
48
|
+
return lines.slice(0, maxLines);
|
|
46
49
|
} catch {
|
|
47
|
-
return
|
|
50
|
+
return [];
|
|
48
51
|
}
|
|
49
52
|
}
|
|
50
53
|
|
|
@@ -65,6 +68,22 @@ function headerMeta(line: string | undefined): { id?: string; path?: string; bra
|
|
|
65
68
|
}
|
|
66
69
|
}
|
|
67
70
|
|
|
71
|
+
/** Detect task-tool helper sessions from the durable early session_init metadata entry. */
|
|
72
|
+
function isInternalSession(lines: readonly string[]): boolean {
|
|
73
|
+
for (const line of lines.slice(1)) {
|
|
74
|
+
if (!line.trim()) continue;
|
|
75
|
+
try {
|
|
76
|
+
const obj = JSON.parse(line) as unknown;
|
|
77
|
+
if (typeof obj === "object" && obj !== null && (obj as { type?: unknown }).type === "session_init") {
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
} catch {
|
|
81
|
+
// Ignore malformed JSONL entries; classification is best-effort.
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
|
|
68
87
|
/**
|
|
69
88
|
* The authoritative session id for a history file: the header `id` when present,
|
|
70
89
|
* else the filename stem with a leading `<timestamp>_` prefix stripped (matching
|
|
@@ -85,7 +104,8 @@ function sessionIdForFile(stem: string, headerId: string | undefined): string {
|
|
|
85
104
|
*/
|
|
86
105
|
export function listRecentSessions(deps: RecentActivityDeps): RecentSessionEntry[] {
|
|
87
106
|
const limit = deps.limit ?? 20;
|
|
88
|
-
const
|
|
107
|
+
const includeInternal = deps.includeInternal ?? true;
|
|
108
|
+
const readInitialLines = deps.readInitialLines ?? defaultReadInitialLines;
|
|
89
109
|
const breadcrumbs = new Set((deps.breadcrumbPaths ?? []).map(p => path.resolve(p)));
|
|
90
110
|
|
|
91
111
|
let projectDirs: string[];
|
|
@@ -114,7 +134,10 @@ export function listRecentSessions(deps: RecentActivityDeps): RecentSessionEntry
|
|
|
114
134
|
} catch {
|
|
115
135
|
continue;
|
|
116
136
|
}
|
|
117
|
-
const
|
|
137
|
+
const initialLines = readInitialLines(file, 8);
|
|
138
|
+
const meta = headerMeta(initialLines[0]);
|
|
139
|
+
const internal = isInternalSession(initialLines);
|
|
140
|
+
if (internal && !includeInternal) continue;
|
|
118
141
|
entries.push({
|
|
119
142
|
sessionId: sessionIdForFile(name.slice(0, -".jsonl".length), meta.id),
|
|
120
143
|
path: meta.path,
|
|
@@ -123,6 +146,7 @@ export function listRecentSessions(deps: RecentActivityDeps): RecentSessionEntry
|
|
|
123
146
|
sessionStateFile: file,
|
|
124
147
|
mtimeMs,
|
|
125
148
|
currentTerminal: breadcrumbs.has(path.resolve(file)) || undefined,
|
|
149
|
+
internal: internal || undefined,
|
|
126
150
|
});
|
|
127
151
|
}
|
|
128
152
|
}
|
|
@@ -97,6 +97,14 @@ export interface TelegramDaemonControlDeps {
|
|
|
97
97
|
sendSignal?: (pid: number, signal: NodeJS.Signals) => void;
|
|
98
98
|
spawn?: TelegramDaemonDeps["spawn"];
|
|
99
99
|
execPath?: string;
|
|
100
|
+
/**
|
|
101
|
+
* Stable process id encoded into freshly-spawned daemon owner ids.
|
|
102
|
+
*
|
|
103
|
+
* The daemon-internal entrypoint rejects numeric owner ids whose process is
|
|
104
|
+
* already gone. `gjc daemon reload` is a short-lived CLI process, so using its
|
|
105
|
+
* own pid can race the child startup and make the replacement exit immediately.
|
|
106
|
+
*/
|
|
107
|
+
ownerPid?: number;
|
|
100
108
|
randomId?: () => string;
|
|
101
109
|
sleep?: (ms: number) => Promise<void>;
|
|
102
110
|
waitStepMs?: number;
|
|
@@ -188,6 +196,7 @@ export class TelegramDaemonController implements BuiltInDaemonController {
|
|
|
188
196
|
fs: this.deps.fs,
|
|
189
197
|
now: this.deps.now,
|
|
190
198
|
pidAlive: this.deps.pidAlive,
|
|
199
|
+
pid: this.deps.ownerPid ?? process.ppid,
|
|
191
200
|
spawn: this.deps.spawn,
|
|
192
201
|
execPath: this.deps.execPath,
|
|
193
202
|
randomId: this.deps.randomId,
|