@gajae-code/coding-agent 0.2.5 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/dist/types/async/job-manager.d.ts +84 -2
  3. package/dist/types/commands/harness.d.ts +37 -0
  4. package/dist/types/config/settings-schema.d.ts +6 -0
  5. package/dist/types/config/settings.d.ts +2 -0
  6. package/dist/types/deep-interview/render-middleware.d.ts +5 -0
  7. package/dist/types/extensibility/custom-tools/types.d.ts +1 -0
  8. package/dist/types/extensibility/extensions/types.d.ts +6 -0
  9. package/dist/types/extensibility/shared-events.d.ts +1 -0
  10. package/dist/types/gjc-runtime/state-graph.d.ts +4 -0
  11. package/dist/types/gjc-runtime/state-migrations.d.ts +24 -0
  12. package/dist/types/gjc-runtime/state-renderer.d.ts +65 -0
  13. package/dist/types/gjc-runtime/state-runtime.d.ts +2 -0
  14. package/dist/types/gjc-runtime/state-validation.d.ts +6 -0
  15. package/dist/types/gjc-runtime/state-writer.d.ts +137 -0
  16. package/dist/types/gjc-runtime/team-runtime.d.ts +81 -7
  17. package/dist/types/gjc-runtime/workflow-manifest.d.ts +54 -0
  18. package/dist/types/harness-control-plane/classifier.d.ts +13 -0
  19. package/dist/types/harness-control-plane/control-endpoint.d.ts +30 -0
  20. package/dist/types/harness-control-plane/finalize.d.ts +47 -0
  21. package/dist/types/harness-control-plane/frame-mapper.d.ts +29 -0
  22. package/dist/types/harness-control-plane/operate.d.ts +35 -0
  23. package/dist/types/harness-control-plane/owner.d.ts +46 -0
  24. package/dist/types/harness-control-plane/preserve.d.ts +19 -0
  25. package/dist/types/harness-control-plane/receipts.d.ts +88 -0
  26. package/dist/types/harness-control-plane/rpc-adapter.d.ts +66 -0
  27. package/dist/types/harness-control-plane/seams.d.ts +21 -0
  28. package/dist/types/harness-control-plane/session-lease.d.ts +65 -0
  29. package/dist/types/harness-control-plane/state-machine.d.ts +19 -0
  30. package/dist/types/harness-control-plane/storage.d.ts +53 -0
  31. package/dist/types/harness-control-plane/types.d.ts +162 -0
  32. package/dist/types/hooks/skill-keywords.d.ts +2 -1
  33. package/dist/types/hooks/skill-state.d.ts +2 -29
  34. package/dist/types/modes/components/hook-selector.d.ts +1 -0
  35. package/dist/types/modes/interactive-mode.d.ts +1 -0
  36. package/dist/types/modes/types.d.ts +1 -0
  37. package/dist/types/sdk.d.ts +2 -0
  38. package/dist/types/session/agent-session.d.ts +8 -0
  39. package/dist/types/skill-state/active-state.d.ts +2 -0
  40. package/dist/types/skill-state/deep-interview-mutation-guard.d.ts +1 -1
  41. package/dist/types/skill-state/workflow-state-contract.d.ts +24 -0
  42. package/dist/types/task/executor.d.ts +3 -0
  43. package/dist/types/task/types.d.ts +55 -3
  44. package/dist/types/tools/subagent.d.ts +11 -1
  45. package/package.json +7 -7
  46. package/src/async/job-manager.ts +298 -6
  47. package/src/cli/auth-broker-cli.ts +1 -0
  48. package/src/cli/config-cli.ts +10 -2
  49. package/src/cli.ts +2 -0
  50. package/src/commands/harness.ts +592 -0
  51. package/src/commands/team.ts +36 -39
  52. package/src/config/settings-schema.ts +7 -0
  53. package/src/config/settings.ts +5 -0
  54. package/src/deep-interview/render-middleware.ts +366 -0
  55. package/src/defaults/gjc/skills/team/SKILL.md +47 -21
  56. package/src/defaults/gjc/skills/ultragoal/SKILL.md +78 -11
  57. package/src/extensibility/custom-tools/types.ts +1 -0
  58. package/src/extensibility/extensions/types.ts +6 -0
  59. package/src/extensibility/shared-events.ts +1 -0
  60. package/src/gjc-runtime/deep-interview-runtime.ts +40 -21
  61. package/src/gjc-runtime/goal-mode-request.ts +11 -3
  62. package/src/gjc-runtime/ralplan-runtime.ts +25 -10
  63. package/src/gjc-runtime/state-graph.ts +86 -0
  64. package/src/gjc-runtime/state-migrations.ts +132 -0
  65. package/src/gjc-runtime/state-renderer.ts +345 -0
  66. package/src/gjc-runtime/state-runtime.ts +733 -21
  67. package/src/gjc-runtime/state-validation.ts +49 -0
  68. package/src/gjc-runtime/state-writer.ts +718 -0
  69. package/src/gjc-runtime/team-runtime.ts +1083 -89
  70. package/src/gjc-runtime/ultragoal-runtime.ts +348 -19
  71. package/src/gjc-runtime/workflow-manifest.generated.json +1497 -0
  72. package/src/gjc-runtime/workflow-manifest.ts +425 -0
  73. package/src/harness-control-plane/classifier.ts +128 -0
  74. package/src/harness-control-plane/control-endpoint.ts +137 -0
  75. package/src/harness-control-plane/finalize.ts +222 -0
  76. package/src/harness-control-plane/frame-mapper.ts +286 -0
  77. package/src/harness-control-plane/operate.ts +225 -0
  78. package/src/harness-control-plane/owner.ts +553 -0
  79. package/src/harness-control-plane/preserve.ts +102 -0
  80. package/src/harness-control-plane/receipts.ts +216 -0
  81. package/src/harness-control-plane/rpc-adapter.ts +276 -0
  82. package/src/harness-control-plane/seams.ts +39 -0
  83. package/src/harness-control-plane/session-lease.ts +388 -0
  84. package/src/harness-control-plane/state-machine.ts +97 -0
  85. package/src/harness-control-plane/storage.ts +257 -0
  86. package/src/harness-control-plane/types.ts +214 -0
  87. package/src/hooks/skill-keywords.ts +4 -2
  88. package/src/hooks/skill-state.ts +24 -41
  89. package/src/internal-urls/docs-index.generated.ts +1 -1
  90. package/src/modes/components/assistant-message.ts +5 -1
  91. package/src/modes/components/hook-selector.ts +72 -2
  92. package/src/modes/controllers/event-controller.ts +71 -6
  93. package/src/modes/controllers/extension-ui-controller.ts +6 -0
  94. package/src/modes/controllers/input-controller.ts +9 -1
  95. package/src/modes/controllers/selector-controller.ts +2 -1
  96. package/src/modes/interactive-mode.ts +1 -0
  97. package/src/modes/types.ts +1 -0
  98. package/src/prompts/agents/executor.md +13 -0
  99. package/src/prompts/tools/subagent.md +33 -3
  100. package/src/sdk.ts +4 -0
  101. package/src/session/agent-session.ts +231 -33
  102. package/src/session/session-manager.ts +13 -1
  103. package/src/skill-state/active-state.ts +58 -65
  104. package/src/skill-state/deep-interview-mutation-guard.ts +91 -13
  105. package/src/skill-state/initial-phase.ts +2 -0
  106. package/src/skill-state/workflow-state-contract.ts +26 -0
  107. package/src/task/executor.ts +50 -8
  108. package/src/task/index.ts +120 -8
  109. package/src/task/render.ts +6 -3
  110. package/src/task/types.ts +56 -3
  111. package/src/tools/ask.ts +28 -7
  112. package/src/tools/subagent.ts +255 -64
package/src/task/index.ts CHANGED
@@ -65,6 +65,18 @@ import {
65
65
  type WorktreeBaseline,
66
66
  } from "./worktree";
67
67
 
68
+ interface TaskResumeDescriptor {
69
+ toolCallId: string;
70
+ params: TaskParams;
71
+ task: TaskItem & { id: string };
72
+ sessionFile: string | null;
73
+ forkContextSeed?: ForkContextSeed;
74
+ agentSource: AgentDefinition["source"];
75
+ }
76
+
77
+ function isTaskResumeDescriptor(value: unknown): value is TaskResumeDescriptor {
78
+ return typeof value === "object" && value !== null && "task" in value && "params" in value;
79
+ }
68
80
  function renderSubagentUserPrompt(assignment: string, simpleMode: TaskSimpleMode): string {
69
81
  return prompt.render(subagentUserPromptTemplate, {
70
82
  assignment: assignment.trim(),
@@ -416,6 +428,50 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
416
428
  };
417
429
 
418
430
  const maxConcurrency = this.session.settings.get("task.maxConcurrency");
431
+ if (typeof manager.setResumeRunner === "function") {
432
+ manager.setResumeRunner((_subagentId, message, resumeDescriptor) => {
433
+ const descriptor = isTaskResumeDescriptor(resumeDescriptor?.data) ? resumeDescriptor.data : undefined;
434
+ if (!descriptor) return undefined;
435
+ const forkSeeds = descriptor.forkContextSeed
436
+ ? new Map([[descriptor.task.id, descriptor.forkContextSeed]])
437
+ : undefined;
438
+ return manager.register(
439
+ "task",
440
+ descriptor.task.id,
441
+ async ({ signal: runSignal }) => {
442
+ const result = await this.#executeSync(
443
+ descriptor.toolCallId,
444
+ { ...descriptor.params, tasks: [descriptor.task] },
445
+ runSignal,
446
+ undefined,
447
+ [descriptor.task.id],
448
+ forkSeeds,
449
+ {
450
+ runMode: message ? "message" : "resume",
451
+ resumeMessage: message,
452
+ sessionFiles: new Map([[descriptor.task.id, descriptor.sessionFile]]),
453
+ },
454
+ );
455
+ const finalText = result.content.find(part => part.type === "text")?.text ?? "(no output)";
456
+ const singleResult = result.details?.results[0];
457
+ return singleResult?.paused ? { kind: "paused" } : finalText;
458
+ },
459
+ {
460
+ id: `${descriptor.task.id}-resume-${Snowflake.next()}`,
461
+ ownerId: this.session.getAgentId?.() ?? undefined,
462
+ metadata: {
463
+ subagent: {
464
+ id: descriptor.task.id,
465
+ agent: descriptor.params.agent,
466
+ agentSource: descriptor.agentSource,
467
+ description: descriptor.task.description,
468
+ assignment: descriptor.task.assignment.trim(),
469
+ },
470
+ },
471
+ },
472
+ );
473
+ });
474
+ }
419
475
  const semaphore = new Semaphore(maxConcurrency);
420
476
  const buildForkContextSeedForTask = async (task: TaskItem): Promise<ForkContextSeed | undefined> => {
421
477
  if (task.inheritContext !== true) return undefined;
@@ -431,6 +487,8 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
431
487
  });
432
488
  };
433
489
  const frozenForkSeeds = new Map<string, ForkContextSeed>();
490
+ const parentSessionFileForBatch = this.session.getSessionFile();
491
+ const batchArtifactsDir = parentSessionFileForBatch ? parentSessionFileForBatch.slice(0, -6) : null;
434
492
 
435
493
  for (let i = 0; i < taskItems.length; i++) {
436
494
  const taskItem = taskItems[i];
@@ -449,6 +507,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
449
507
  const singleParams: TaskParams = { ...params, tasks: [taskItem] };
450
508
  const label = uniqueId;
451
509
  try {
510
+ const subtaskSessionFile = batchArtifactsDir ? path.join(batchArtifactsDir, `${uniqueId}.jsonl`) : null;
452
511
  const jobId = manager.register(
453
512
  "task",
454
513
  label,
@@ -478,15 +537,20 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
478
537
  undefined,
479
538
  [uniqueId],
480
539
  frozenForkSeeds,
540
+ {
541
+ sessionFiles: new Map([[uniqueId, subtaskSessionFile]]),
542
+ },
481
543
  );
482
544
  const finalText = result.content.find(part => part.type === "text")?.text ?? "(no output)";
483
545
  const singleResult = result.details?.results[0];
484
546
  if (progress) {
485
- progress.status = singleResult?.aborted
486
- ? "aborted"
487
- : (singleResult?.exitCode ?? 0) === 0
488
- ? "completed"
489
- : "failed";
547
+ progress.status = singleResult?.paused
548
+ ? "paused"
549
+ : singleResult?.aborted
550
+ ? "aborted"
551
+ : (singleResult?.exitCode ?? 0) === 0
552
+ ? "completed"
553
+ : "failed";
490
554
  progress.durationMs = singleResult?.durationMs ?? Math.max(0, Date.now() - startedAt);
491
555
  progress.tokens = singleResult?.tokens ?? 0;
492
556
  progress.contextTokens = singleResult?.contextTokens;
@@ -517,6 +581,9 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
517
581
  `Background task batch complete: ${completedJobs}/${taskItems.length} finished.`,
518
582
  );
519
583
  }
584
+ if (singleResult?.paused) {
585
+ return { kind: "paused" };
586
+ }
520
587
  return finalText;
521
588
  } catch (error) {
522
589
  if (progress) {
@@ -568,6 +635,31 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
568
635
  },
569
636
  );
570
637
  startedJobs.push({ jobId, taskId: taskItem.id });
638
+ if (typeof manager.registerResumeDescriptor === "function") {
639
+ manager.registerResumeDescriptor({
640
+ subagentId: uniqueId,
641
+ ownerId: this.session.getAgentId?.() ?? undefined,
642
+ data: {
643
+ toolCallId: _toolCallId,
644
+ params,
645
+ task: { ...taskItem, id: uniqueId },
646
+ sessionFile: subtaskSessionFile,
647
+ forkContextSeed: frozenForkSeed,
648
+ agentSource: fallbackAgentSource,
649
+ } satisfies TaskResumeDescriptor,
650
+ });
651
+ }
652
+ if (typeof manager.registerSubagentRecord === "function") {
653
+ manager.registerSubagentRecord({
654
+ subagentId: uniqueId,
655
+ ownerId: this.session.getAgentId?.() ?? undefined,
656
+ currentJobId: jobId,
657
+ historicalJobIds: [],
658
+ status: manager.getJob(jobId)?.status ?? "running",
659
+ sessionFile: subtaskSessionFile,
660
+ resumable: !!batchArtifactsDir,
661
+ });
662
+ }
571
663
  } catch (error) {
572
664
  const message = error instanceof Error ? error.message : String(error);
573
665
  failedSchedules.push(`${taskItem.id}: ${message}`);
@@ -637,6 +729,11 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
637
729
  onUpdate?: AgentToolUpdateCallback<TaskToolDetails>,
638
730
  preAllocatedIds?: string[],
639
731
  prebuiltForkContextSeeds?: ReadonlyMap<string, ForkContextSeed>,
732
+ executionOverrides?: {
733
+ runMode?: "initial" | "resume" | "message";
734
+ resumeMessage?: string;
735
+ sessionFiles?: ReadonlyMap<string, string | null>;
736
+ },
640
737
  ): Promise<AgentToolResult<TaskToolDetails>> {
641
738
  const startTime = Date.now();
642
739
  const { agents, projectAgentsDir } = await discoverAgents(this.session.cwd);
@@ -996,8 +1093,17 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
996
1093
  });
997
1094
  };
998
1095
 
999
- const runTask = async (task: (typeof tasksWithUniqueIds)[number], index: number) => {
1096
+ const runTask = async (
1097
+ task: (typeof tasksWithUniqueIds)[number],
1098
+ index: number,
1099
+ overrides?: {
1100
+ runMode?: "initial" | "resume" | "message";
1101
+ resumeMessage?: string;
1102
+ sessionFile?: string | null;
1103
+ },
1104
+ ) => {
1000
1105
  const forkContextSeed = prebuiltForkContextSeeds?.get(task.id) ?? (await buildForkContextSeed(task));
1106
+ const taskSessionFile = overrides?.sessionFile ?? executionOverrides?.sessionFiles?.get(task.id) ?? null;
1001
1107
  if (!isIsolated) {
1002
1108
  return runSubprocess({
1003
1109
  cwd: this.session.cwd,
@@ -1008,12 +1114,15 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
1008
1114
  description: task.description,
1009
1115
  index,
1010
1116
  id: task.id,
1117
+ runMode: overrides?.runMode ?? executionOverrides?.runMode,
1118
+ resumeMessage: overrides?.resumeMessage ?? executionOverrides?.resumeMessage,
1119
+ subagentId: task.id,
1011
1120
  taskDepth,
1012
1121
  modelOverride,
1013
1122
  parentActiveModelPattern,
1014
1123
  thinkingLevel: thinkingLevelOverride,
1015
1124
  outputSchema: effectiveOutputSchema,
1016
- sessionFile,
1125
+ sessionFile: taskSessionFile,
1017
1126
  persistArtifacts: !!artifactsDir,
1018
1127
  artifactsDir: effectiveArtifactsDir,
1019
1128
  contextFile: contextFilePath,
@@ -1063,12 +1172,15 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
1063
1172
  description: task.description,
1064
1173
  index,
1065
1174
  id: task.id,
1175
+ runMode: overrides?.runMode ?? executionOverrides?.runMode,
1176
+ resumeMessage: overrides?.resumeMessage ?? executionOverrides?.resumeMessage,
1177
+ subagentId: task.id,
1066
1178
  taskDepth,
1067
1179
  modelOverride,
1068
1180
  parentActiveModelPattern,
1069
1181
  thinkingLevel: thinkingLevelOverride,
1070
1182
  outputSchema: effectiveOutputSchema,
1071
- sessionFile,
1183
+ sessionFile: taskSessionFile,
1072
1184
  persistArtifacts: !!artifactsDir,
1073
1185
  artifactsDir: effectiveArtifactsDir,
1074
1186
  contextFile: contextFilePath,
@@ -47,6 +47,8 @@ function getStatusIcon(status: AgentProgress["status"], theme: Theme, spinnerFra
47
47
  return formatStatusIcon("error", theme);
48
48
  case "aborted":
49
49
  return formatStatusIcon("aborted", theme);
50
+ case "paused":
51
+ return formatStatusIcon("pending", theme);
50
52
  }
51
53
  }
52
54
 
@@ -611,9 +613,10 @@ function renderAgentProgress(
611
613
  if (progress.retryState && progress.status === "running") {
612
614
  const remainingMs = Math.max(0, progress.retryState.startedAtMs + progress.retryState.delayMs - Date.now());
613
615
  const waitLabel = remainingMs > 0 ? `in ${formatDuration(remainingMs)}` : "now";
614
- const summary =
615
- `retrying ${progress.retryState.attempt}/${progress.retryState.maxAttempts} ${waitLabel}: ` +
616
- truncateToWidth(replaceTabs(progress.retryState.errorMessage), 60);
616
+ const attemptLabel = progress.retryState.unbounded
617
+ ? `attempt ${progress.retryState.attempt}`
618
+ : `${progress.retryState.attempt}/${progress.retryState.maxAttempts}`;
619
+ const summary = `retrying ${attemptLabel} ${waitLabel}: ${truncateToWidth(replaceTabs(progress.retryState.errorMessage), 60)}`;
617
620
  lines.push(`${continuePrefix}${theme.tree.hook} ${theme.fg("warning", summary)}`);
618
621
  } else if (progress.retryFailure && progress.status !== "running") {
619
622
  const summary = `auto-retry gave up after ${progress.retryFailure.attempt} attempt${
package/src/task/types.ts CHANGED
@@ -53,7 +53,7 @@ export interface SubagentLifecyclePayload {
53
53
  agent: string;
54
54
  agentSource: AgentSource;
55
55
  description?: string;
56
- status: "started" | "completed" | "failed" | "aborted";
56
+ status: "started" | "completed" | "failed" | "aborted" | "paused";
57
57
  sessionFile?: string;
58
58
  index: number;
59
59
  }
@@ -192,7 +192,7 @@ export interface AgentProgress {
192
192
  id: string;
193
193
  agent: string;
194
194
  agentSource: AgentSource;
195
- status: "pending" | "running" | "completed" | "failed" | "aborted";
195
+ status: "pending" | "running" | "completed" | "failed" | "aborted" | "paused";
196
196
  task: string;
197
197
  assignment?: string;
198
198
  description?: string;
@@ -230,6 +230,7 @@ export interface AgentProgress {
230
230
  retryState?: {
231
231
  attempt: number;
232
232
  maxAttempts: number;
233
+ unbounded?: boolean;
233
234
  delayMs: number;
234
235
  errorMessage: string;
235
236
  startedAtMs: number;
@@ -279,6 +280,7 @@ export interface SingleResult {
279
280
  error?: string;
280
281
  aborted?: boolean;
281
282
  abortReason?: string;
283
+ paused?: boolean;
282
284
  /** Aggregated usage from the subprocess, accumulated incrementally from message_end events. */
283
285
  usage?: Usage;
284
286
  /** Output path for the task result */
@@ -315,8 +317,59 @@ export interface TaskToolDetails {
315
317
  outputPaths?: string[];
316
318
  progress?: AgentProgress[];
317
319
  async?: {
318
- state: "running" | "completed" | "failed";
320
+ state: "running" | "paused" | "queued" | "completed" | "failed";
319
321
  jobId: string;
320
322
  type: "task";
321
323
  };
322
324
  }
325
+ /**
326
+ * Persisted per-turn / per-subagent token record (Phase 0 instrumentation).
327
+ *
328
+ * Additive: this does not alter any existing task result shape. It is the
329
+ * durable, model-independent unit the deterministic orchestration-token
330
+ * benchmark (`@gajae-code/orchestration-token-benchmark`) consumes to measure
331
+ * token efficiency without any live-model calls.
332
+ */
333
+ export interface TaskTokenLog {
334
+ /** Subagent id, or "root" for the orchestrator's own turn. */
335
+ subagentId: string;
336
+ /** Agent name for attribution, when known. */
337
+ agent?: string;
338
+ /** 1-based turn index within the subagent's session. */
339
+ turn: number;
340
+ /** ISO-8601 timestamp the turn completed. */
341
+ at: string;
342
+ /** Cost-bearing input tokens (excludes cache reads), mirrors `Usage.input`. */
343
+ input: number;
344
+ /** Total output tokens for the turn, mirrors `Usage.output`. */
345
+ output: number;
346
+ /** Tokens read from the prompt cache, mirrors `Usage.cacheRead`. */
347
+ cacheRead: number;
348
+ /** Tokens written to the prompt cache, mirrors `Usage.cacheWrite`. */
349
+ cacheWrite: number;
350
+ /** input + output + cacheRead + cacheWrite. */
351
+ totalTokens: number;
352
+ /** Latest per-turn context-window occupancy, when known. */
353
+ contextTokens?: number;
354
+ /** Estimated USD cost for the turn, when known. */
355
+ cost?: number;
356
+ /** Model id used for the turn, when known. */
357
+ model?: string;
358
+ }
359
+
360
+ /**
361
+ * Deterministic aggregate token metrics computed from a set of `TaskTokenLog`
362
+ * entries. The cache-hit-rate field is the primary prompt-cache signal called
363
+ * out by the prefix-stability invariant (see the approved plan).
364
+ */
365
+ export interface TaskTokenMetrics {
366
+ /** Number of token-log entries aggregated. */
367
+ turns: number;
368
+ inputTokens: number;
369
+ outputTokens: number;
370
+ cacheReadTokens: number;
371
+ cacheWriteTokens: number;
372
+ totalTokens: number;
373
+ /** cacheRead / (input + cacheRead); 0 when there is no input-class traffic. */
374
+ cacheHitRate: number;
375
+ }
package/src/tools/ask.ts CHANGED
@@ -28,6 +28,7 @@ import {
28
28
  } from "@gajae-code/tui";
29
29
  import { prompt, untilAborted } from "@gajae-code/utils";
30
30
  import * as z from "zod/v4";
31
+ import { formatDeepInterviewSelectorPrompt, renderDeepInterviewAskQuestion } from "../deep-interview/render-middleware";
31
32
  import type { RenderResultOptions } from "../extensibility/custom-tools/types";
32
33
  import { getMarkdownTheme, type Theme, theme } from "../modes/theme/theme";
33
34
  import askDescription from "../prompts/tools/ask.md" with { type: "text" };
@@ -84,6 +85,7 @@ export interface AskToolDetails {
84
85
 
85
86
  const OTHER_OPTION = "Other (type your own)";
86
87
  const RECOMMENDED_SUFFIX = " (Recommended)";
88
+ const DEEP_INTERVIEW_SELECTOR_SCROLL_TITLE_ROWS = 12;
87
89
 
88
90
  function getDoneOptionLabel(): string {
89
91
  return `${theme.status.success} Done selecting`;
@@ -138,6 +140,7 @@ interface AskSingleQuestionOptions {
138
140
  signal?: AbortSignal;
139
141
  initialSelection?: Pick<SelectionResult, "selectedOptions" | "customInput">;
140
142
  navigation?: NavigationControls;
143
+ scrollTitleRows?: number;
141
144
  }
142
145
 
143
146
  interface UIContext {
@@ -150,6 +153,7 @@ interface UIContext {
150
153
  signal?: AbortSignal;
151
154
  outline?: boolean;
152
155
  wrapFocused?: boolean;
156
+ scrollTitleRows?: number;
153
157
  onTimeout?: () => void;
154
158
  onLeft?: () => void;
155
159
  onRight?: () => void;
@@ -171,7 +175,7 @@ async function askSingleQuestion(
171
175
  multi: boolean,
172
176
  options: AskSingleQuestionOptions = {},
173
177
  ): Promise<SelectionResult> {
174
- const { recommended, timeout, signal, initialSelection, navigation } = options;
178
+ const { recommended, timeout, signal, initialSelection, navigation, scrollTitleRows } = options;
175
179
  const doneLabel = getDoneOptionLabel();
176
180
  let selectedOptions = [...(initialSelection?.selectedOptions ?? [])];
177
181
  let customInput = initialSelection?.customInput;
@@ -187,15 +191,17 @@ async function askSingleQuestion(
187
191
  timeoutTriggered = true;
188
192
  };
189
193
  let navigationAction: "back" | "forward" | undefined;
190
- const helpText = navigation
194
+ const baseHelpText = navigation
191
195
  ? "up/down navigate enter select ←/→ question esc cancel"
192
196
  : "up/down navigate enter select esc cancel";
197
+ const helpText = scrollTitleRows === undefined ? baseHelpText : `${baseHelpText} PgUp/PgDn scroll question`;
193
198
  const dialogOptions = {
194
199
  initialIndex,
195
200
  timeout,
196
201
  signal,
197
202
  outline: true,
198
203
  wrapFocused: true,
204
+ scrollTitleRows,
199
205
  onTimeout,
200
206
  helpText,
201
207
  onLeft: navigation?.allowBack
@@ -454,9 +460,11 @@ export class AskTool implements AgentTool<typeof askSchema, AskToolDetails> {
454
460
  ) => {
455
461
  const optionLabels = q.options.map(o => o.label);
456
462
  try {
463
+ const deepInterviewPrompt = formatDeepInterviewSelectorPrompt(q.question);
464
+ const displayQuestion = deepInterviewPrompt ?? q.question;
457
465
  const { selectedOptions, customInput, navigation, cancelled, timedOut } = await askSingleQuestion(
458
466
  ui,
459
- q.question,
467
+ displayQuestion,
460
468
  optionLabels,
461
469
  q.multi ?? false,
462
470
  {
@@ -465,6 +473,7 @@ export class AskTool implements AgentTool<typeof askSchema, AskToolDetails> {
465
473
  signal,
466
474
  initialSelection: options?.previous,
467
475
  navigation: options?.navigation,
476
+ scrollTitleRows: deepInterviewPrompt === null ? undefined : DEEP_INTERVIEW_SELECTOR_SCROLL_TITLE_ROWS,
468
477
  },
469
478
  );
470
479
  return { optionLabels, selectedOptions, customInput, navigation, cancelled, timedOut };
@@ -659,7 +668,10 @@ export const askToolRenderer = {
659
668
  container.addChild(
660
669
  new Text(` ${uiTheme.fg("dim", qBranch)} ${uiTheme.fg("dim", `[${q.id}]`)}${metaStr}`, 0, 0),
661
670
  );
662
- container.addChild(new Markdown(q.question, 3, 0, mdTheme, accentStyle));
671
+ container.addChild(
672
+ renderDeepInterviewAskQuestion(q.question, uiTheme) ??
673
+ new Markdown(q.question, 3, 0, mdTheme, accentStyle),
674
+ );
663
675
 
664
676
  const qOptions = q.options;
665
677
  if (qOptions?.length) {
@@ -688,7 +700,10 @@ export const askToolRenderer = {
688
700
  if (args.multi) meta.push("multi");
689
701
  if (args.options?.length) meta.push(`options:${args.options.length}`);
690
702
  container.addChild(new Text(`${label}${formatMeta(meta, uiTheme)}`, 0, 0));
691
- container.addChild(new Markdown(args.question, 1, 0, mdTheme, accentStyle));
703
+ container.addChild(
704
+ renderDeepInterviewAskQuestion(args.question, uiTheme) ??
705
+ new Markdown(args.question, 1, 0, mdTheme, accentStyle),
706
+ );
692
707
 
693
708
  const options = args.options;
694
709
  if (options?.length) {
@@ -752,7 +767,10 @@ export const askToolRenderer = {
752
767
  container.addChild(
753
768
  new Text(` ${uiTheme.fg("dim", branch)} ${statusIcon} ${uiTheme.fg("dim", `[${r.id}]`)}`, 0, 0),
754
769
  );
755
- container.addChild(new Markdown(r.question, 3, 0, mdTheme, accentStyle));
770
+ container.addChild(
771
+ renderDeepInterviewAskQuestion(r.question, uiTheme) ??
772
+ new Markdown(r.question, 3, 0, mdTheme, accentStyle),
773
+ );
756
774
 
757
775
  const answerLines: string[] = [];
758
776
  for (let j = 0; j < r.selectedOptions.length; j++) {
@@ -795,7 +813,10 @@ export const askToolRenderer = {
795
813
  const header = renderStatusLine({ icon: hasSelection ? "success" : "warning", title: "Ask" }, uiTheme);
796
814
  const container = new Container();
797
815
  container.addChild(new Text(header, 0, 0));
798
- container.addChild(new Markdown(details.question, 1, 0, mdTheme, accentStyle));
816
+ container.addChild(
817
+ renderDeepInterviewAskQuestion(details.question, uiTheme) ??
818
+ new Markdown(details.question, 1, 0, mdTheme, accentStyle),
819
+ );
799
820
 
800
821
  const answerLines: string[] = [];
801
822
  if (details.selectedOptions && details.selectedOptions.length > 0) {