@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.
Files changed (123) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/README.md +2 -0
  3. package/dist/types/config/keybindings.d.ts +11 -5
  4. package/dist/types/config/settings-schema.d.ts +10 -1
  5. package/dist/types/defaults/gjc-defaults.d.ts +7 -0
  6. package/dist/types/extensibility/extensions/types.d.ts +11 -0
  7. package/dist/types/gjc-runtime/launch-tmux.d.ts +10 -0
  8. package/dist/types/gjc-runtime/ralplan-runtime.d.ts +3 -2
  9. package/dist/types/gjc-runtime/restricted-role-agent-bash.d.ts +1 -0
  10. package/dist/types/gjc-runtime/state-writer.d.ts +1 -0
  11. package/dist/types/gjc-runtime/ultragoal-guard.d.ts +4 -0
  12. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +0 -4
  13. package/dist/types/modes/components/custom-editor.d.ts +2 -1
  14. package/dist/types/modes/components/hook-selector.d.ts +5 -0
  15. package/dist/types/modes/components/skill-message.d.ts +1 -0
  16. package/dist/types/modes/controllers/command-controller.d.ts +1 -0
  17. package/dist/types/modes/controllers/input-controller.d.ts +4 -0
  18. package/dist/types/modes/interactive-mode.d.ts +1 -0
  19. package/dist/types/modes/prompt-action-autocomplete.d.ts +2 -0
  20. package/dist/types/modes/shared/agent-wire/deep-interview-gate.d.ts +20 -6
  21. package/dist/types/modes/slash-command-visibility.d.ts +1 -0
  22. package/dist/types/modes/types.d.ts +1 -0
  23. package/dist/types/notifications/html-format.d.ts +2 -0
  24. package/dist/types/notifications/recent-activity.d.ts +5 -1
  25. package/dist/types/notifications/telegram-daemon-control.d.ts +8 -0
  26. package/dist/types/notifications/telegram-reference.d.ts +4 -0
  27. package/dist/types/notifications/threaded-render.d.ts +1 -0
  28. package/dist/types/sdk.d.ts +1 -1
  29. package/dist/types/session/agent-session.d.ts +8 -5
  30. package/dist/types/slash-commands/types.d.ts +2 -0
  31. package/dist/types/tools/ask.d.ts +2 -0
  32. package/dist/types/utils/cmux-workspace.d.ts +52 -0
  33. package/dist/types/web/search/index.d.ts +4 -2
  34. package/dist/types/web/search/provider.d.ts +14 -0
  35. package/dist/types/web/search/providers/utils.d.ts +42 -13
  36. package/dist/types/web/search/render.d.ts +1 -0
  37. package/package.json +7 -7
  38. package/src/cli/update-cli.ts +24 -0
  39. package/src/cli/web-search-cli.ts +5 -7
  40. package/src/cli.ts +1 -1
  41. package/src/commands/launch.ts +1 -1
  42. package/src/commands/ralplan.ts +1 -0
  43. package/src/config/keybindings.ts +14 -5
  44. package/src/config/model-profiles.ts +16 -0
  45. package/src/config/settings-schema.ts +11 -1
  46. package/src/coordinator-mcp/server.ts +9 -3
  47. package/src/defaults/gjc/skills/deep-interview/SKILL.md +26 -2
  48. package/src/defaults/gjc/skills/ralplan/SKILL.md +16 -14
  49. package/src/defaults/gjc/skills/ultragoal/SKILL.md +37 -112
  50. package/src/defaults/gjc-defaults.ts +16 -0
  51. package/src/extensibility/extensions/runner.ts +2 -1
  52. package/src/extensibility/extensions/types.ts +11 -0
  53. package/src/gjc-runtime/launch-tmux.ts +209 -26
  54. package/src/gjc-runtime/ralplan-runtime.ts +22 -6
  55. package/src/gjc-runtime/restricted-role-agent-bash.ts +1 -0
  56. package/src/gjc-runtime/state-runtime.ts +8 -5
  57. package/src/gjc-runtime/state-writer.ts +5 -5
  58. package/src/gjc-runtime/ultragoal-guard.ts +139 -19
  59. package/src/gjc-runtime/ultragoal-runtime.ts +16 -124
  60. package/src/gjc-runtime/workflow-manifest.generated.json +0 -8
  61. package/src/gjc-runtime/workflow-manifest.ts +0 -1
  62. package/src/hooks/skill-state.ts +33 -73
  63. package/src/internal-urls/docs-index.generated.ts +6 -4
  64. package/src/main.ts +2 -2
  65. package/src/migrate/executor.ts +23 -6
  66. package/src/modes/components/custom-editor.ts +8 -0
  67. package/src/modes/components/hook-selector.ts +21 -3
  68. package/src/modes/components/model-selector.ts +6 -2
  69. package/src/modes/components/settings-selector.ts +47 -5
  70. package/src/modes/components/skill-message.ts +18 -10
  71. package/src/modes/components/status-line/presets.ts +11 -0
  72. package/src/modes/controllers/command-controller.ts +23 -0
  73. package/src/modes/controllers/extension-ui-controller.ts +18 -2
  74. package/src/modes/controllers/input-controller.ts +41 -9
  75. package/src/modes/interactive-mode.ts +16 -7
  76. package/src/modes/prompt-action-autocomplete.ts +23 -1
  77. package/src/modes/shared/agent-wire/deep-interview-gate.ts +53 -9
  78. package/src/modes/slash-command-visibility.ts +9 -0
  79. package/src/modes/types.ts +1 -0
  80. package/src/modes/utils/hotkeys-markdown.ts +5 -2
  81. package/src/modes/utils/ui-helpers.ts +76 -3
  82. package/src/notifications/html-format.ts +75 -1
  83. package/src/notifications/index.ts +12 -1
  84. package/src/notifications/recent-activity.ts +32 -8
  85. package/src/notifications/telegram-daemon-control.ts +9 -0
  86. package/src/notifications/telegram-daemon.ts +68 -25
  87. package/src/notifications/telegram-reference.ts +36 -14
  88. package/src/notifications/threaded-render.ts +14 -6
  89. package/src/prompts/agents/architect.md +7 -4
  90. package/src/prompts/agents/critic.md +15 -6
  91. package/src/prompts/agents/planner.md +8 -3
  92. package/src/prompts/system/system-prompt.md +5 -1
  93. package/src/prompts/tools/bash.md +1 -1
  94. package/src/sdk.ts +6 -11
  95. package/src/session/agent-session.ts +60 -17
  96. package/src/slash-commands/builtin-registry.ts +30 -10
  97. package/src/slash-commands/types.ts +2 -0
  98. package/src/tools/ask.ts +136 -16
  99. package/src/tools/bash.ts +24 -8
  100. package/src/utils/cmux-workspace.ts +233 -0
  101. package/src/utils/title-generator.ts +2 -0
  102. package/src/web/kagi.ts +1 -1
  103. package/src/web/parallel.ts +2 -2
  104. package/src/web/search/index.ts +137 -52
  105. package/src/web/search/provider.ts +80 -0
  106. package/src/web/search/providers/anthropic.ts +1 -1
  107. package/src/web/search/providers/brave.ts +1 -1
  108. package/src/web/search/providers/codex.ts +1 -1
  109. package/src/web/search/providers/duckduckgo.ts +1 -1
  110. package/src/web/search/providers/exa.ts +1 -1
  111. package/src/web/search/providers/gemini.ts +7 -3
  112. package/src/web/search/providers/jina.ts +1 -1
  113. package/src/web/search/providers/kimi.ts +6 -1
  114. package/src/web/search/providers/openai-compatible.ts +1 -1
  115. package/src/web/search/providers/parallel.ts +1 -1
  116. package/src/web/search/providers/perplexity.ts +2 -2
  117. package/src/web/search/providers/searxng.ts +1 -1
  118. package/src/web/search/providers/synthetic.ts +1 -1
  119. package/src/web/search/providers/tavily.ts +1 -1
  120. package/src/web/search/providers/utils.ts +67 -17
  121. package/src/web/search/providers/xai.ts +1 -1
  122. package/src/web/search/providers/zai.ts +1 -1
  123. 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?.priority ?? 0,
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 interface DeepInterviewGateAnswer {
66
- selected: string[];
67
- other?: boolean;
68
- custom?: string;
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 DeepInterviewGateAnswer;
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
- "answer must be { selected: string[]; other?: boolean; custom?: string }",
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
+ }
@@ -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
- "| `#` | Open prompt actions |",
55
- "| `/` | Slash commands |",
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.ctx.isKnownSlashCommand(message.text)) {
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.ctx.isKnownSlashCommand(queuedMessages[i].text)) {
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.ctx.session.prompt(message.text);
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
- if (!diff && !tokenUsage && !modelId) return;
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
- readFirstLine?: (file: string) => string | undefined;
42
+ readInitialLines?: (file: string, maxLines: number) => string[];
39
43
  }
40
44
 
41
- function defaultReadFirstLine(file: string): string | undefined {
45
+ function defaultReadInitialLines(file: string, maxLines: number): string[] {
42
46
  try {
43
- const buf = fs.readFileSync(file, "utf8");
44
- const nl = buf.indexOf("\n");
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 undefined;
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 readFirstLine = deps.readFirstLine ?? defaultReadFirstLine;
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 meta = headerMeta(readFirstLine(file));
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,