topchester-ai 0.22.0 → 0.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.mjs CHANGED
@@ -98,7 +98,7 @@ async function enqueueFileMutation(path, mutate) {
98
98
  }
99
99
  //#endregion
100
100
  //#region src/agent/instructions.ts
101
- const PROJECT_INSTRUCTION_FILENAMES = ["AGENTS.override.md", "AGENTS.md"];
101
+ const PROJECT_INSTRUCTION_FILENAMES = ["AGENTS.md", "AGENTS.override.md"];
102
102
  async function resolveProjectInstructions(workspaceRoot, options = {}) {
103
103
  const filenames = getProjectInstructionFilenames(options);
104
104
  if (filenames.length === 0) return {
@@ -114,21 +114,23 @@ async function resolveProjectInstructions(workspaceRoot, options = {}) {
114
114
  const sources = [];
115
115
  let remainingBytes = Math.max(0, maxTotalBytes);
116
116
  for (const directory of directories) {
117
- const candidate = await readFirstInstructionCandidate(resolvedWorkspace, directory, filenames, options.logger);
118
- if (!candidate) continue;
119
- const scopedContentLimit = Math.min(Math.max(0, maxBytesPerFile), remainingBytes);
120
- const content = truncateUtf8(candidate.content, scopedContentLimit);
121
- const truncated = content !== candidate.content;
122
- remainingBytes -= Buffer.byteLength(content, "utf8");
123
- sources.push({
124
- path: candidate.absolutePath,
125
- relativePath: candidate.relativePath,
126
- scopePath: formatScopePath(relative(resolvedWorkspace, directory)),
127
- depth: getScopeDepth(relative(resolvedWorkspace, directory)),
128
- bytes: candidate.bytes,
129
- truncated,
130
- content
131
- });
117
+ const candidates = await readInstructionCandidates(resolvedWorkspace, directory, filenames, options.logger);
118
+ if (candidates.length === 0) continue;
119
+ for (const candidate of candidates) {
120
+ const scopedContentLimit = Math.min(Math.max(0, maxBytesPerFile), remainingBytes);
121
+ const content = truncateUtf8(candidate.content, scopedContentLimit);
122
+ const truncated = content !== candidate.content;
123
+ remainingBytes -= Buffer.byteLength(content, "utf8");
124
+ sources.push({
125
+ path: candidate.absolutePath,
126
+ relativePath: candidate.relativePath,
127
+ scopePath: formatScopePath(relative(resolvedWorkspace, directory)),
128
+ depth: getScopeDepth(relative(resolvedWorkspace, directory)),
129
+ bytes: candidate.bytes,
130
+ truncated,
131
+ content
132
+ });
133
+ }
132
134
  }
133
135
  options.logger?.debug({
134
136
  event: "project_instructions_resolved",
@@ -212,7 +214,8 @@ function getInstructionDirectories(resolvedWorkspace, targetDirectory) {
212
214
  }
213
215
  return directories;
214
216
  }
215
- async function readFirstInstructionCandidate(resolvedWorkspace, directory, filenames, logger) {
217
+ async function readInstructionCandidates(resolvedWorkspace, directory, filenames, logger) {
218
+ const candidates = [];
216
219
  for (const filename of filenames) {
217
220
  const absolutePath = resolve(directory, filename);
218
221
  const relativePath = formatRelativePath(relative(resolvedWorkspace, absolutePath));
@@ -235,12 +238,12 @@ async function readFirstInstructionCandidate(resolvedWorkspace, directory, filen
235
238
  }, "project instruction skipped");
236
239
  continue;
237
240
  }
238
- return {
241
+ candidates.push({
239
242
  absolutePath,
240
243
  relativePath,
241
244
  content,
242
245
  bytes: bytes.byteLength
243
- };
246
+ });
244
247
  } catch (error) {
245
248
  const code = typeof error === "object" && error && "code" in error ? String(error.code) : void 0;
246
249
  if (code === "ENOENT" || code === "ENOTDIR") continue;
@@ -252,6 +255,7 @@ async function readFirstInstructionCandidate(resolvedWorkspace, directory, filen
252
255
  }, "project instruction skipped");
253
256
  }
254
257
  }
258
+ return candidates;
255
259
  }
256
260
  function truncateUtf8(content, maxBytes) {
257
261
  if (maxBytes <= 0) return "";
@@ -5269,6 +5273,7 @@ const hookHandlerSchema = z.object({
5269
5273
  type: z.literal("command").optional(),
5270
5274
  command: z.string().min(1),
5271
5275
  timeoutMs: hookTimeoutMsSchema.optional(),
5276
+ statusMessage: z.string().min(1).optional(),
5272
5277
  matcher: hookMatcherSchema
5273
5278
  }).strict();
5274
5279
  const canonicalHooksConfigSchema = z.object({
@@ -7563,6 +7568,14 @@ function toolCallMessage(call, label, resultSummary) {
7563
7568
  resultSummary
7564
7569
  };
7565
7570
  }
7571
+ function hookStatusMessage(label, eventName, statusMessage) {
7572
+ return {
7573
+ kind: "hook_status",
7574
+ label,
7575
+ ...eventName === void 0 ? {} : { eventName },
7576
+ ...statusMessage === void 0 ? {} : { statusMessage }
7577
+ };
7578
+ }
7566
7579
  function subagentMessage(message) {
7567
7580
  return {
7568
7581
  kind: "subagent",
@@ -7579,6 +7592,7 @@ const DEFAULT_MODAL_VISIBLE_ACTION_LIMIT = 16;
7579
7592
  function renderChatMessage(message, options = {}) {
7580
7593
  if (message.kind === "modal") return renderChatModal(message, options.selectedActionIndex, options.maxModalHeight);
7581
7594
  if (message.kind === "tool_call") return renderToolCallMessage(message);
7595
+ if (message.kind === "hook_status") return renderHookStatusMessage(message);
7582
7596
  if (message.kind === "subagent") return renderSubagentMessage(message);
7583
7597
  if (message.kind === "thinking") return message.text.split("\n").map((line) => ui.muted(line));
7584
7598
  if (message.text.length === 0) return [""];
@@ -7613,6 +7627,9 @@ function renderToolCallMessage(message) {
7613
7627
  const visibleLabel = message.resultSummary && !message.label.includes(message.resultSummary) ? `${message.label} ${message.resultSummary}` : message.label;
7614
7628
  return [` ${ui.muted(expandTabs(visibleLabel))}`];
7615
7629
  }
7630
+ function renderHookStatusMessage(message) {
7631
+ return [` ${ui.muted(expandTabs(message.label))}`];
7632
+ }
7616
7633
  function renderSubagentMessage(message) {
7617
7634
  const label = message.title ?? shortSessionId(message.sessionId);
7618
7635
  switch (message.status) {
@@ -7770,6 +7787,12 @@ const toolCallPayloadSchema = z.object({
7770
7787
  label: z.string(),
7771
7788
  call: z.record(z.string(), jsonValueSchema)
7772
7789
  });
7790
+ const hookStatusPayloadSchema = z.object({
7791
+ kind: z.literal("hook_status"),
7792
+ eventName: z.string(),
7793
+ statusMessage: z.string(),
7794
+ label: z.string()
7795
+ });
7773
7796
  const taskPlanItemPayloadSchema = z.object({
7774
7797
  text: z.string(),
7775
7798
  status: z.enum([
@@ -7836,6 +7859,7 @@ const subagentFailedPayloadSchema = subagentLifecycleBasePayloadSchema.extend({
7836
7859
  const sessionEventPayloadSchema = z.discriminatedUnion("kind", [
7837
7860
  messagePayloadSchema,
7838
7861
  toolCallPayloadSchema,
7862
+ hookStatusPayloadSchema,
7839
7863
  taskPlanPayloadSchema,
7840
7864
  instructionContextPayloadSchema,
7841
7865
  statusPayloadSchema,
@@ -8008,6 +8032,9 @@ function rehydrateSession(events) {
8008
8032
  case "tool_call":
8009
8033
  messages.push(toolCallMessage(event.call, event.label));
8010
8034
  break;
8035
+ case "hook_status":
8036
+ messages.push(hookStatusMessage(event.label, event.eventName, event.statusMessage));
8037
+ break;
8011
8038
  case "task_plan":
8012
8039
  taskPlan = {
8013
8040
  items: event.items,
@@ -8634,6 +8661,14 @@ const agentEvent = {
8634
8661
  label
8635
8662
  };
8636
8663
  },
8664
+ hookStatus(eventName, statusMessage) {
8665
+ return {
8666
+ type: "hook_status",
8667
+ eventName,
8668
+ statusMessage,
8669
+ label: `🪝 hook>${formatHookEventName(eventName)}: ${statusMessage}`
8670
+ };
8671
+ },
8637
8672
  taskPlan(plan) {
8638
8673
  return {
8639
8674
  type: "task_plan",
@@ -8694,6 +8729,9 @@ function choiceAction(label, value) {
8694
8729
  value
8695
8730
  };
8696
8731
  }
8732
+ function formatHookEventName(eventName) {
8733
+ return eventName.replace(/([a-z])([A-Z])/gu, "$1-$2").toLowerCase();
8734
+ }
8697
8735
  //#endregion
8698
8736
  //#region src/tui/keys.ts
8699
8737
  function isUpKey(data) {
@@ -8783,6 +8821,7 @@ var PromptHistory = class {
8783
8821
  };
8784
8822
  //#endregion
8785
8823
  //#region src/tui/status.ts
8824
+ const STARTUP_PROMPT_HINT = "Prompt hint: Enter sends, Shift+Enter adds a line, / opens commands, ↑↓ browse history.";
8786
8825
  function getStartupThreadMessages(context) {
8787
8826
  const assignments = context.config.models?.assignments ?? {};
8788
8827
  const providers = context.config.models?.providers ?? {};
@@ -8932,6 +8971,7 @@ var ChatLayout = class {
8932
8971
  promptCursor = 0;
8933
8972
  status = "ready";
8934
8973
  knowledgeStatus;
8974
+ startupHintLine;
8935
8975
  ephemeralLine;
8936
8976
  taskPlanNoticeLine;
8937
8977
  noticeLine;
@@ -8993,6 +9033,9 @@ var ChatLayout = class {
8993
9033
  isReady() {
8994
9034
  return this.status === "ready";
8995
9035
  }
9036
+ setStartupHintLine(line) {
9037
+ this.startupHintLine = line;
9038
+ }
8996
9039
  setEphemeralLine(line) {
8997
9040
  this.ephemeralLine = line;
8998
9041
  }
@@ -9026,6 +9069,7 @@ var ChatLayout = class {
9026
9069
  this.promptCursor = 0;
9027
9070
  this.status = "ready";
9028
9071
  this.knowledgeStatus = void 0;
9072
+ this.startupHintLine = void 0;
9029
9073
  this.ephemeralLine = void 0;
9030
9074
  this.taskPlanNoticeLine = void 0;
9031
9075
  this.noticeLine = void 0;
@@ -9056,6 +9100,7 @@ var ChatLayout = class {
9056
9100
  case "system":
9057
9101
  case "thinking":
9058
9102
  case "tool_call":
9103
+ case "hook_status":
9059
9104
  case "subagent":
9060
9105
  case "modal": return [];
9061
9106
  }
@@ -9110,6 +9155,7 @@ var ChatLayout = class {
9110
9155
  const spacer = index === this.messages.length - 1 ? [] : [padThreadLine("", width)];
9111
9156
  return [...this.renderThreadMessageLines(messageLines, innerWidth, width, message.kind === "user"), ...spacer];
9112
9157
  });
9158
+ if (this.startupHintLine) lines.push(...this.renderThreadMessageLines([` ${ui.muted(this.startupHintLine)}`], innerWidth, width, false));
9113
9159
  if (this.ephemeralLine) lines.push(...this.renderThreadMessageLines([` ${this.ephemeralLine}`], innerWidth, width, false));
9114
9160
  if (this.taskPlanNoticeLine) lines.push(...this.renderThreadMessageLines([` ${this.taskPlanNoticeLine}`], innerWidth, width, false));
9115
9161
  if (this.noticeLine) lines.push(...this.renderThreadMessageLines([` ${this.noticeLine}`], innerWidth, width, false));
@@ -9123,6 +9169,7 @@ var ChatLayout = class {
9123
9169
  });
9124
9170
  }
9125
9171
  renderPrompt(width) {
9172
+ const slashSuggestions = this.getSlashSuggestions();
9126
9173
  const top = `┌${"─".repeat(Math.max(0, width - 2))}┐`;
9127
9174
  const bottom = `└${"─".repeat(Math.max(0, width - 2))}┘`;
9128
9175
  const prefix = "> ";
@@ -9131,7 +9178,7 @@ var ChatLayout = class {
9131
9178
  const statusInnerWidth = Math.max(1, width - 2);
9132
9179
  const status = truncateToWidth(` ${formatStatusLine(this.folderName, this.modelLabel, this.status, this.knowledgeStatus, statusInnerWidth)} `, width, "…", true);
9133
9180
  return [
9134
- ...this.renderSlashSuggestions(width),
9181
+ ...this.renderSlashSuggestions(width, slashSuggestions),
9135
9182
  ...this.renderTaskPlan(width),
9136
9183
  top,
9137
9184
  ...inputLines.map((line, index) => `│ ${index === 0 ? prefix : " "}${padPromptInputLine(line, innerWidth)} │`),
@@ -9190,8 +9237,7 @@ var ChatLayout = class {
9190
9237
  if (!this.taskPlan) return [];
9191
9238
  return formatTaskPlanForTui(this.taskPlan, Math.max(1, width));
9192
9239
  }
9193
- renderSlashSuggestions(width) {
9194
- const suggestions = this.getSlashSuggestions();
9240
+ renderSlashSuggestions(width, suggestions = this.getSlashSuggestions()) {
9195
9241
  if (suggestions.length === 0 || this.promptHint) return [];
9196
9242
  this.activeSlashSuggestionIndex = Math.min(this.activeSlashSuggestionIndex, suggestions.length - 1);
9197
9243
  const innerWidth = Math.max(1, width - 4);
@@ -9542,6 +9588,8 @@ var ChatLayout = class {
9542
9588
  }
9543
9589
  submitUserInput(message) {
9544
9590
  this.setTaskPlanNotice(void 0);
9591
+ this.setStartupHintLine(void 0);
9592
+ this.setEphemeralLine(void 0);
9545
9593
  this.promptHistory.add(message);
9546
9594
  if (message.startsWith("/")) this.submitCommand?.(message);
9547
9595
  else this.submitMessage?.(message);
@@ -9625,6 +9673,11 @@ async function runTopchesterHooks(context, event, payload, options = {}) {
9625
9673
  };
9626
9674
  for (const handler of handlers) {
9627
9675
  result.handlerCount += 1;
9676
+ const statusMessage = handler.statusMessage?.trim();
9677
+ if (statusMessage) options.onHookStart?.({
9678
+ event,
9679
+ statusMessage
9680
+ });
9628
9681
  const handlerResult = await runCommandHandler(context, event, payload, handler, options);
9629
9682
  result.contexts.push(...handlerResult.contexts);
9630
9683
  result.messages.push(...handlerResult.messages);
@@ -10197,6 +10250,12 @@ function runtimeEventToSessionPayload(event) {
10197
10250
  label: event.label,
10198
10251
  call: event.call
10199
10252
  };
10253
+ case "hook_status": return {
10254
+ kind: "hook_status",
10255
+ eventName: event.eventName,
10256
+ statusMessage: event.statusMessage,
10257
+ label: event.label
10258
+ };
10200
10259
  case "task_plan": return {
10201
10260
  kind: "task_plan",
10202
10261
  items: event.plan.items,
@@ -10838,15 +10897,27 @@ var TopchesterAgentRuntime = class TopchesterAgentRuntime {
10838
10897
  const sessionKey = session?.sessionId ?? `workspace:${this.context.workspaceRoot}`;
10839
10898
  if (this.startedHookSessionKeys.has(sessionKey)) return [];
10840
10899
  this.startedHookSessionKeys.add(sessionKey);
10900
+ const startedEvents = [];
10841
10901
  const result = await this.runHookEvent("SessionStart", this.createBaseHookPayload("SessionStart", session, {
10842
10902
  isResumed: Boolean(options.isResumed),
10843
10903
  taskStartAlias: "TaskStart"
10844
- }), { abortSignal: options.abortSignal });
10845
- return this.hookResultToEvents(result);
10904
+ }), {
10905
+ abortSignal: options.abortSignal,
10906
+ onHookStart: (status) => {
10907
+ startedEvents.push(agentEvent.hookStatus(status.event, status.statusMessage));
10908
+ }
10909
+ });
10910
+ return [...startedEvents, ...this.hookResultToEvents(result)];
10846
10911
  }
10847
10912
  async runPreCompactHooks(session, options = {}) {
10848
- const result = await this.runHookEvent("PreCompact", this.createBaseHookPayload("PreCompact", session, { reason: options.reason ?? "Compaction is about to start." }), { abortSignal: options.abortSignal });
10849
- return this.hookResultToEvents(result);
10913
+ const startedEvents = [];
10914
+ const result = await this.runHookEvent("PreCompact", this.createBaseHookPayload("PreCompact", session, { reason: options.reason ?? "Compaction is about to start." }), {
10915
+ abortSignal: options.abortSignal,
10916
+ onHookStart: (status) => {
10917
+ startedEvents.push(agentEvent.hookStatus(status.event, status.statusMessage));
10918
+ }
10919
+ });
10920
+ return [...startedEvents, ...this.hookResultToEvents(result)];
10850
10921
  }
10851
10922
  /**
10852
10923
  * Streams one user chat turn through the agent loop. It builds the model
@@ -10862,11 +10933,13 @@ var TopchesterAgentRuntime = class TopchesterAgentRuntime {
10862
10933
  async *submitMessageStream(conversation, message, abortSignal, options = {}) {
10863
10934
  const session = options.session ?? this.options.session;
10864
10935
  for (const event of await this.runSessionStartHooks(session, { abortSignal })) yield event;
10865
- const userPromptHook = await this.runHookEvent("UserPromptSubmit", this.createBaseHookPayload("UserPromptSubmit", session, {
10936
+ const userPromptHookRun = this.startHookEvent("UserPromptSubmit", this.createBaseHookPayload("UserPromptSubmit", session, {
10866
10937
  prompt: { text: message },
10867
10938
  prompt_text: message,
10868
10939
  user_prompt: message
10869
10940
  }), { abortSignal });
10941
+ for await (const event of userPromptHookRun.statusEvents) yield event;
10942
+ const userPromptHook = await userPromptHookRun.result;
10870
10943
  for (const event of this.hookResultToEvents(userPromptHook)) yield event;
10871
10944
  if (userPromptHook.blocked || userPromptHook.stopped) {
10872
10945
  const interruption = userPromptHook.blocked ?? userPromptHook.stopped;
@@ -10995,7 +11068,7 @@ var TopchesterAgentRuntime = class TopchesterAgentRuntime {
10995
11068
  }
10996
11069
  const finalMessage = finalText.trim() || "I got an empty response from the model.";
10997
11070
  yield agentEvent.assistantMessage(finalMessage, formatAgentMessageMeta(result.modelId, totalDurationMs, tokenUsageTotals));
10998
- for (const event of await this.runStopHookEvents(session, finalMessage, "completed", abortSignal)) yield event;
11071
+ for await (const event of this.streamStopHookEvents(session, finalMessage, "completed", abortSignal)) yield event;
10999
11072
  yield agentEvent.status("ready");
11000
11073
  return;
11001
11074
  }
@@ -11019,7 +11092,9 @@ var TopchesterAgentRuntime = class TopchesterAgentRuntime {
11019
11092
  for (let batchIndex = 0; batchIndex < batch.length; batchIndex += 1) {
11020
11093
  const call = batch[batchIndex];
11021
11094
  const resultIndex = index + batchIndex;
11022
- const preHook = await this.runPreToolUseHook(call, modelToolCalls[resultIndex]?.id, session, abortSignal);
11095
+ const preHookRun = this.startPreToolUseHook(call, modelToolCalls[resultIndex]?.id, session, abortSignal);
11096
+ for await (const event of preHookRun.statusEvents) yield event;
11097
+ const preHook = await preHookRun.result;
11023
11098
  for (const event of this.hookResultToEvents(preHook)) yield event;
11024
11099
  if (preHook.stopped) {
11025
11100
  if (preHook.messages.length === 0) yield agentEvent.systemMessage(preHook.stopped.message);
@@ -11065,7 +11140,9 @@ var TopchesterAgentRuntime = class TopchesterAgentRuntime {
11065
11140
  const toolResult = taskResults[index];
11066
11141
  for (const event of createInstructionContextEventsFromToolResult(toolResult, persistedProjectInstructionKeys)) yield event;
11067
11142
  yield agentEvent.toolCall(call, formatToolCallMessage(call, toolResult));
11068
- const postHook = await this.runPostToolUseHook(call, modelToolCalls[index]?.id, toolResult, session, abortSignal);
11143
+ const postHookRun = this.startPostToolUseHook(call, modelToolCalls[index]?.id, toolResult, session, abortSignal);
11144
+ for await (const event of postHookRun.statusEvents) yield event;
11145
+ const postHook = await postHookRun.result;
11069
11146
  for (const event of this.hookResultToEvents(postHook)) yield event;
11070
11147
  postHookContexts.push(...postHook.contexts);
11071
11148
  if (postHook.stopped) {
@@ -11085,7 +11162,9 @@ var TopchesterAgentRuntime = class TopchesterAgentRuntime {
11085
11162
  const postHookContexts = [];
11086
11163
  for (let index = 0; index < parallelCalls.length; index += 1) {
11087
11164
  const call = parallelCalls[index];
11088
- const preHook = await this.runPreToolUseHook(call, modelToolCalls[index]?.id, session, abortSignal);
11165
+ const preHookRun = this.startPreToolUseHook(call, modelToolCalls[index]?.id, session, abortSignal);
11166
+ for await (const event of preHookRun.statusEvents) yield event;
11167
+ const preHook = await preHookRun.result;
11089
11168
  for (const event of this.hookResultToEvents(preHook)) yield event;
11090
11169
  if (preHook.stopped) {
11091
11170
  if (preHook.messages.length === 0) yield agentEvent.systemMessage(preHook.stopped.message);
@@ -11123,7 +11202,9 @@ var TopchesterAgentRuntime = class TopchesterAgentRuntime {
11123
11202
  const toolResult = parallelResults[index];
11124
11203
  for (const event of createInstructionContextEventsFromToolResult(toolResult, persistedProjectInstructionKeys)) yield event;
11125
11204
  yield agentEvent.toolCall(call, formatToolCallMessage(call, toolResult));
11126
- const postHook = await this.runPostToolUseHook(call, modelToolCalls[index]?.id, toolResult, session, abortSignal);
11205
+ const postHookRun = this.startPostToolUseHook(call, modelToolCalls[index]?.id, toolResult, session, abortSignal);
11206
+ for await (const event of postHookRun.statusEvents) yield event;
11207
+ const postHook = await postHookRun.result;
11127
11208
  for (const event of this.hookResultToEvents(postHook)) yield event;
11128
11209
  postHookContexts.push(...postHook.contexts);
11129
11210
  if (postHook.stopped) {
@@ -11141,11 +11222,13 @@ var TopchesterAgentRuntime = class TopchesterAgentRuntime {
11141
11222
  if (suppressiblePlanTodoAnswer !== void 0) {
11142
11223
  const finalMessage = suppressiblePlanTodoAnswer || "I got an empty response from the model.";
11143
11224
  yield agentEvent.assistantMessage(finalMessage, formatAgentMessageMeta(result.modelId, totalDurationMs, tokenUsageTotals));
11144
- for (const event of await this.runStopHookEvents(session, finalMessage, "completed", abortSignal)) yield event;
11225
+ for await (const event of this.streamStopHookEvents(session, finalMessage, "completed", abortSignal)) yield event;
11145
11226
  yield agentEvent.status("ready");
11146
11227
  return;
11147
11228
  }
11148
- const preHook = await this.runPreToolUseHook(executableToolCall, toolCall.id, session, abortSignal);
11229
+ const preHookRun = this.startPreToolUseHook(executableToolCall, toolCall.id, session, abortSignal);
11230
+ for await (const event of preHookRun.statusEvents) yield event;
11231
+ const preHook = await preHookRun.result;
11149
11232
  for (const event of this.hookResultToEvents(preHook)) yield event;
11150
11233
  let toolResult;
11151
11234
  if (preHook.stopped) {
@@ -11183,7 +11266,9 @@ var TopchesterAgentRuntime = class TopchesterAgentRuntime {
11183
11266
  for (const event of createInstructionContextEventsFromToolResult(toolResult, persistedProjectInstructionKeys)) yield event;
11184
11267
  yield agentEvent.toolCall(executableToolCall, formatToolCallMessage(executableToolCall, toolResult));
11185
11268
  if (!isToolErrorResult(toolResult) && toolResult.tool === "plan_todo") yield agentEvent.taskPlan(toolResult.plan);
11186
- const postHook = await this.runPostToolUseHook(executableToolCall, toolCall.id, toolResult, session, abortSignal);
11269
+ const postHookRun = this.startPostToolUseHook(executableToolCall, toolCall.id, toolResult, session, abortSignal);
11270
+ for await (const event of postHookRun.statusEvents) yield event;
11271
+ const postHook = await postHookRun.result;
11187
11272
  for (const event of this.hookResultToEvents(postHook)) yield event;
11188
11273
  if (postHook.stopped) {
11189
11274
  if (postHook.messages.length === 0) yield agentEvent.systemMessage(postHook.stopped.message);
@@ -11195,7 +11280,7 @@ var TopchesterAgentRuntime = class TopchesterAgentRuntime {
11195
11280
  }
11196
11281
  const finalMessage = "I stopped because the tool loop ended unexpectedly.";
11197
11282
  yield agentEvent.assistantMessage(finalMessage, formatAgentMessageMeta(lastModelId, totalDurationMs, tokenUsageTotals));
11198
- for (const event of await this.runStopHookEvents(session, finalMessage, "failed", abortSignal)) yield event;
11283
+ for await (const event of this.streamStopHookEvents(session, finalMessage, "failed", abortSignal)) yield event;
11199
11284
  yield agentEvent.status("ready");
11200
11285
  }
11201
11286
  /**
@@ -11210,29 +11295,45 @@ var TopchesterAgentRuntime = class TopchesterAgentRuntime {
11210
11295
  }
11211
11296
  return events;
11212
11297
  }
11213
- async runPreToolUseHook(call, toolCallId, session, abortSignal) {
11214
- return this.runHookEvent("PreToolUse", this.createToolHookPayload("PreToolUse", call, toolCallId, session), {
11298
+ startPreToolUseHook(call, toolCallId, session, abortSignal) {
11299
+ return this.startHookEvent("PreToolUse", this.createToolHookPayload("PreToolUse", call, toolCallId, session), {
11215
11300
  toolName: call.tool,
11216
11301
  abortSignal
11217
11302
  });
11218
11303
  }
11219
- async runPostToolUseHook(call, toolCallId, result, session, abortSignal) {
11220
- return this.runHookEvent("PostToolUse", this.createToolHookPayload("PostToolUse", call, toolCallId, session, { result }), {
11304
+ startPostToolUseHook(call, toolCallId, result, session, abortSignal) {
11305
+ return this.startHookEvent("PostToolUse", this.createToolHookPayload("PostToolUse", call, toolCallId, session, { result }), {
11221
11306
  toolName: call.tool,
11222
11307
  abortSignal
11223
11308
  });
11224
11309
  }
11225
- async runStopHookEvents(session, finalMessage, status, abortSignal) {
11226
- const result = await this.runHookEvent("Stop", this.createBaseHookPayload("Stop", session, {
11310
+ async *streamStopHookEvents(session, finalMessage, status, abortSignal) {
11311
+ const hookRun = this.startHookEvent("Stop", this.createBaseHookPayload("Stop", session, {
11227
11312
  taskCompleteAlias: "TaskComplete",
11228
11313
  finalMessage,
11229
11314
  status
11230
11315
  }), { abortSignal });
11231
- return this.hookResultToEvents(result);
11316
+ for await (const event of hookRun.statusEvents) yield event;
11317
+ const result = await hookRun.result;
11318
+ for (const event of this.hookResultToEvents(result)) yield event;
11232
11319
  }
11233
11320
  async runHookEvent(event, payload, options = {}) {
11234
11321
  return runTopchesterHooks(this.context, event, payload, options);
11235
11322
  }
11323
+ startHookEvent(event, payload, options = {}) {
11324
+ const queue = createRuntimeEventQueue();
11325
+ return {
11326
+ statusEvents: queue,
11327
+ result: this.runHookEvent(event, payload, {
11328
+ ...options,
11329
+ onHookStart: (status) => {
11330
+ queue.push(agentEvent.hookStatus(status.event, status.statusMessage));
11331
+ }
11332
+ }).finally(() => {
11333
+ queue.close();
11334
+ })
11335
+ };
11336
+ }
11236
11337
  createBaseHookPayload(event, session, extra = {}) {
11237
11338
  return {
11238
11339
  hook_event_name: event,
@@ -11240,6 +11341,7 @@ var TopchesterAgentRuntime = class TopchesterAgentRuntime {
11240
11341
  cwd: this.context.workspaceRoot,
11241
11342
  workspaceRoot: this.context.workspaceRoot,
11242
11343
  source: "topchester",
11344
+ ...this.createHookModelPayload("agent.primary"),
11243
11345
  ...session ? {
11244
11346
  session_id: session.sessionId,
11245
11347
  sessionId: session.sessionId,
@@ -11253,6 +11355,33 @@ var TopchesterAgentRuntime = class TopchesterAgentRuntime {
11253
11355
  ...extra
11254
11356
  };
11255
11357
  }
11358
+ createHookModelPayload(purpose) {
11359
+ const resolveModel = this.context.modelGateway.resolveModel;
11360
+ if (typeof resolveModel !== "function") return {};
11361
+ try {
11362
+ const resolved = resolveModel.call(this.context.modelGateway, purpose);
11363
+ const modelRef = `${resolved.providerId}/${resolved.modelId}`;
11364
+ return {
11365
+ model_purpose: resolved.purpose,
11366
+ model_provider: resolved.providerId,
11367
+ model_id: resolved.modelId,
11368
+ model_ref: modelRef,
11369
+ model: {
11370
+ purpose: resolved.purpose,
11371
+ providerId: resolved.providerId,
11372
+ modelId: resolved.modelId,
11373
+ ref: modelRef
11374
+ }
11375
+ };
11376
+ } catch (error) {
11377
+ this.context.logger.debug({
11378
+ event: "hook_model_resolution_skipped",
11379
+ purpose,
11380
+ error: error instanceof Error ? error.message : String(error)
11381
+ }, "hook model metadata unavailable");
11382
+ return {};
11383
+ }
11384
+ }
11256
11385
  createToolHookPayload(event, call, toolCallId, session, extra = {}) {
11257
11386
  return this.createBaseHookPayload(event, session, {
11258
11387
  tool_name: call.tool,
@@ -11481,6 +11610,7 @@ function renderRuntimeEvent(event) {
11481
11610
  switch (event.type) {
11482
11611
  case "message": return [event.role === "assistant" ? agentMessage(event.text, event.meta) : systemMessage(event.text)];
11483
11612
  case "tool_call": return [toolCallMessage(event.call, event.label)];
11613
+ case "hook_status": return [hookStatusMessage(event.label, event.eventName, event.statusMessage)];
11484
11614
  case "knowledge_status": return [systemMessage([`KB status: ${formatKnowledgePathStatus(event.status)}${formatKbPathSource(event.status)}`, event.guidance].filter(Boolean).join("\n"))];
11485
11615
  case "choice": return [modalMessage({
11486
11616
  tone: event.tone,
@@ -11520,6 +11650,11 @@ function formatForwardedSubagentEvent(sessionId, event) {
11520
11650
  sessionId,
11521
11651
  text: event.label
11522
11652
  })];
11653
+ if (event.type === "hook_status") return [subagentMessage({
11654
+ status: "event",
11655
+ sessionId,
11656
+ text: event.label
11657
+ })];
11523
11658
  return [];
11524
11659
  }
11525
11660
  function formatKbPathSource(status) {
@@ -11718,6 +11853,12 @@ function chatMessageToSessionPayload(message) {
11718
11853
  };
11719
11854
  if (message.kind === "thinking") return;
11720
11855
  if (message.kind === "subagent") return;
11856
+ if (message.kind === "hook_status") return message.eventName && message.statusMessage ? {
11857
+ kind: "hook_status",
11858
+ eventName: message.eventName,
11859
+ statusMessage: message.statusMessage,
11860
+ label: message.label
11861
+ } : void 0;
11721
11862
  if (message.kind === "modal") return {
11722
11863
  kind: "choice",
11723
11864
  tone: message.tone,
@@ -11969,6 +12110,7 @@ var TopchesterTuiShell = class {
11969
12110
  }
11970
12111
  });
11971
12112
  app.setTaskPlan(this.options.initialTaskPlan);
12113
+ if (!isResumed) app.setStartupHintLine(STARTUP_PROMPT_HINT);
11972
12114
  app.setSubmitMessage((message) => {
11973
12115
  this.startBackgroundTask(app, tui, "Chat", () => this.submitChatMessage(app, tui, message));
11974
12116
  });
@@ -12580,6 +12722,7 @@ var TopchesterTuiShell = class {
12580
12722
  await this.appendStartupRuntimeEvents(session, messages, await this.runtime.runSessionStartHooks?.(session, { isResumed: false }) ?? []);
12581
12723
  await this.appendStartupRuntimeEvents(session, messages, await this.runtime.checkProjectInstructions?.() ?? []);
12582
12724
  app.resetForNewSession(messages);
12725
+ app.setStartupHintLine(STARTUP_PROMPT_HINT);
12583
12726
  tui.requestRender();
12584
12727
  await this.checkAgent(app, tui);
12585
12728
  }
@@ -12787,6 +12930,7 @@ async function loadConversation(workspaceRoot, resume) {
12787
12930
  case "system":
12788
12931
  case "thinking":
12789
12932
  case "tool_call":
12933
+ case "hook_status":
12790
12934
  case "subagent":
12791
12935
  case "modal": return [];
12792
12936
  }
@@ -12819,6 +12963,10 @@ function printPlainEvent(event) {
12819
12963
  console.log(event.label);
12820
12964
  return;
12821
12965
  }
12966
+ if (event.type === "hook_status") {
12967
+ console.log(event.label);
12968
+ return;
12969
+ }
12822
12970
  if (event.type === "knowledge_status" && event.guidance) {
12823
12971
  console.log(event.guidance);
12824
12972
  return;