@mrciphersmith/keryx 0.2.139 → 0.2.143

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 (3) hide show
  1. package/README.md +10 -0
  2. package/dist/cli.js +844 -79
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -248,6 +248,16 @@ What is in it today:
248
248
  session that keeps its ancestry, without editing a transcript by hand.
249
249
  `/status` is the session inspector (identity, context window and limits when
250
250
  the provider reported them); `/session-info` and `/info` are not aliases.
251
+ - **Plans that survive the turn.** For multi-step work, the agent can keep a
252
+ structured execution plan in the session. The TUI shows pending, active,
253
+ completed, blocked, and skipped steps in the sidebar, and restored sessions
254
+ continue from the same plan instead of rebuilding it from transcript text.
255
+ - **Theme-aware rich transcripts.** Assistant prose, headings, emphasis,
256
+ inline code, fenced code, diffs, and GFM pipe tables use semantic colors
257
+ derived from the selected `/theme`. Use a language fence such as
258
+ ```` ```typescript ```` for syntax color, ```` ```diff ```` for addition and
259
+ deletion rows, or ```` ```text ````/```` ```txt ```` when content must remain
260
+ literal, including terminal output and ASCII diagrams.
251
261
  - **Responsive busy-turn UX.** A running main turn can be interrupted with
252
262
  `/interrupt`; additional prompts are queued and answered as read-only side workers
253
263
  (`side-1`) in the TUI so the shell stays usable under long-running turns.
package/dist/cli.js CHANGED
@@ -23987,7 +23987,7 @@ async function openSlateAtomic(dir, mintAttemptId, build) {
23987
23987
  if (existing !== undefined) {
23988
23988
  await archiveIfExistsLocked(dir, mintAttemptId());
23989
23989
  }
23990
- const next = build();
23990
+ const next = build(existing);
23991
23991
  await writeFileAtomic(slatePath(dir), `${JSON.stringify(next, null, 2)}
23992
23992
  `);
23993
23993
  await rm7(path93.join(dir, "terminal-state.json"), { force: true });
@@ -24089,6 +24089,123 @@ var init_slate = __esm(() => {
24089
24089
  ];
24090
24090
  });
24091
24091
 
24092
+ // src/session/execution-plan.ts
24093
+ function clonePlan(plan) {
24094
+ return { revision: plan.revision, items: plan.items.map((item) => ({ ...item })) };
24095
+ }
24096
+ function validateItems(items) {
24097
+ const ids = new Set;
24098
+ let active = 0;
24099
+ return items.map((item) => {
24100
+ const id = item.id.trim();
24101
+ const title = item.title.trim();
24102
+ if (id.length === 0)
24103
+ throw new ExecutionPlanValidationError("item id must be non-empty");
24104
+ if (ids.has(id))
24105
+ throw new ExecutionPlanValidationError(`duplicate item id: ${id}`);
24106
+ ids.add(id);
24107
+ if (title.length === 0)
24108
+ throw new ExecutionPlanValidationError(`item ${id} has an empty title`);
24109
+ if (!EXECUTION_PLAN_STATUSES.includes(item.status)) {
24110
+ throw new ExecutionPlanValidationError(`item ${id} has unknown status ${JSON.stringify(item.status)}`);
24111
+ }
24112
+ if (item.status === "in_progress" && ++active > 1) {
24113
+ throw new ExecutionPlanValidationError("at most one item may be in_progress");
24114
+ }
24115
+ return { id, title, status: item.status };
24116
+ });
24117
+ }
24118
+ function notify(dir, plan) {
24119
+ for (const listener of listeners) {
24120
+ try {
24121
+ listener(dir, clonePlan(plan));
24122
+ } catch {}
24123
+ }
24124
+ }
24125
+ function subscribeExecutionPlans(listener) {
24126
+ listeners.add(listener);
24127
+ return () => listeners.delete(listener);
24128
+ }
24129
+ async function getExecutionPlan(dir) {
24130
+ const plan = (await readSlate(dir))?.executionPlan;
24131
+ return plan === undefined ? undefined : clonePlan(plan);
24132
+ }
24133
+ async function setExecutionPlan(dir, input) {
24134
+ const items = validateItems(input.items);
24135
+ let result;
24136
+ await writeSlate(dir, (prev) => {
24137
+ if (prev === undefined)
24138
+ throw new ExecutionPlanValidationError("no open Slate");
24139
+ const actual = prev.executionPlan?.revision ?? 0;
24140
+ if (input.expectedRevision !== actual)
24141
+ throw new ExecutionPlanConflictError(input.expectedRevision, actual);
24142
+ result = { revision: actual + 1, items };
24143
+ return { ...prev, executionPlan: result };
24144
+ });
24145
+ const plan = clonePlan(result);
24146
+ notify(dir, plan);
24147
+ return plan;
24148
+ }
24149
+ async function updateExecutionPlan(dir, input) {
24150
+ let result;
24151
+ await writeSlate(dir, (prev) => {
24152
+ if (prev === undefined)
24153
+ throw new ExecutionPlanValidationError("no open Slate");
24154
+ const current = prev.executionPlan;
24155
+ const actual = current?.revision ?? 0;
24156
+ if (input.expectedRevision !== actual)
24157
+ throw new ExecutionPlanConflictError(input.expectedRevision, actual);
24158
+ if (current === undefined)
24159
+ throw new ExecutionPlanValidationError("no execution plan exists");
24160
+ if (!EXECUTION_PLAN_STATUSES.includes(input.status)) {
24161
+ throw new ExecutionPlanValidationError(`unknown status ${JSON.stringify(input.status)}`);
24162
+ }
24163
+ if (!current.items.some((item) => item.id === input.itemId)) {
24164
+ throw new ExecutionPlanValidationError(`unknown item id: ${input.itemId}`);
24165
+ }
24166
+ const items = validateItems(current.items.map((item) => item.id === input.itemId ? { ...item, status: input.status } : item));
24167
+ result = { revision: actual + 1, items };
24168
+ return { ...prev, executionPlan: result };
24169
+ });
24170
+ const plan = clonePlan(result);
24171
+ notify(dir, plan);
24172
+ return plan;
24173
+ }
24174
+ function hasActionableExecutionPlanItems(plan) {
24175
+ return plan?.items.some((item) => item.status === "pending" || item.status === "in_progress") ?? false;
24176
+ }
24177
+ function renderExecutionPlanSnapshot(plan, maxItems = 7) {
24178
+ if (plan === undefined || plan.items.length === 0)
24179
+ return;
24180
+ const rows = plan.items.slice(0, Math.max(0, maxItems)).map((item) => {
24181
+ const title = item.title.length > 120 ? `${item.title.slice(0, 119)}\u2026` : item.title;
24182
+ return `- ${item.id} [${item.status}]: ${title}`;
24183
+ });
24184
+ if (plan.items.length > rows.length)
24185
+ rows.push(`- \u2026 ${plan.items.length - rows.length} more item(s)`);
24186
+ return `Current execution plan (revision ${plan.revision}):
24187
+ ${rows.join(`
24188
+ `)}`;
24189
+ }
24190
+ var EXECUTION_PLAN_STATUSES, listeners, ExecutionPlanConflictError, ExecutionPlanValidationError;
24191
+ var init_execution_plan = __esm(() => {
24192
+ init_slate();
24193
+ EXECUTION_PLAN_STATUSES = ["pending", "in_progress", "completed", "blocked", "skipped"];
24194
+ listeners = new Set;
24195
+ ExecutionPlanConflictError = class ExecutionPlanConflictError extends Error {
24196
+ constructor(expected, actual) {
24197
+ super(`execution plan revision conflict: expected ${expected}, current ${actual}`);
24198
+ this.name = "ExecutionPlanConflictError";
24199
+ }
24200
+ };
24201
+ ExecutionPlanValidationError = class ExecutionPlanValidationError extends Error {
24202
+ constructor(message) {
24203
+ super(`invalid execution plan: ${message}`);
24204
+ this.name = "ExecutionPlanValidationError";
24205
+ }
24206
+ };
24207
+ });
24208
+
24092
24209
  // src/flow/store.ts
24093
24210
  import { createHash as createHash16 } from "crypto";
24094
24211
  import { appendFile as appendFile3, mkdir as mkdir32, readFile as readFile49, readdir as readdir12 } from "fs/promises";
@@ -28849,7 +28966,12 @@ async function openSlate(opts) {
28849
28966
  cwd: opts.cwd,
28850
28967
  ...opts.runtime !== undefined ? { runtime: opts.runtime } : {}
28851
28968
  });
28852
- return openSlateAtomic(opts.dir, opts.mintAttemptId, () => ({ anchors, course: {}, seeds: [] }));
28969
+ return openSlateAtomic(opts.dir, opts.mintAttemptId, (existing) => ({
28970
+ anchors,
28971
+ course: {},
28972
+ seeds: [],
28973
+ ...existing?.executionPlan === undefined ? {} : { executionPlan: existing.executionPlan }
28974
+ }));
28853
28975
  }
28854
28976
  async function closeSlate(dir, mintAttemptId) {
28855
28977
  await archiveSlate(dir, mintAttemptId());
@@ -28891,7 +29013,12 @@ async function ensureSlateOpened(ref, mintAttemptId, runtime) {
28891
29013
  if (isSlateSessionDetached(ref)) {
28892
29014
  return;
28893
29015
  }
28894
- await openSlateAtomic(ref.dir, mintAttemptId, () => ({ anchors, course: {}, seeds: [] }));
29016
+ await openSlateAtomic(ref.dir, mintAttemptId, (existing) => ({
29017
+ anchors,
29018
+ course: {},
29019
+ seeds: [],
29020
+ ...existing?.executionPlan === undefined ? {} : { executionPlan: existing.executionPlan }
29021
+ }));
28895
29022
  ref.opened = true;
28896
29023
  }
28897
29024
  async function closeSlateSession(ref, mintAttemptId) {
@@ -29360,7 +29487,7 @@ function buildAgentSystemInstruction(orient, ctx = {}) {
29360
29487
  ` + locateRule + "- read_file returns a bounded window starting at `start_line` (default 1). A large file " + "is read by paging: its truncation notice names the start_line to continue from, and a " + `line number from ${hasGraph ? "search_code or graph_symbol" : "search_code"} can be read directly \u2014 do not re-read the ` + `same head hoping for more.
29361
29488
  ` + "- Prefer ONE correct shell_exec over many exploratory tool calls when the user asks " + `to run a known keryx workflow.
29362
29489
  ` + "- Tool-call budget: shell_exec, file-mutating shell, workspace_create/workspace_propose, and spawn_subagent " + "all share ONE small per-turn pool (distinct non-read actions), separate from the much larger read-tool pool. " + (hasGraph ? "search_code/graph_*/memory_search/read_wiki/wiki_*/test_related/health_status/flow_status/repomap/read_file/" : "search_code/read_file/") + "list_dir do NOT touch it. Conserve the small pool: batch multiple shell steps into ONE call with `&&` instead " + "of issuing them one at a time, get a command's arguments right the first time instead of trying variants, and " + "for any check covered by a read tool above, use that tool instead of shelling out to the equivalent `keryx \u2026` " + `CLI command.
29363
- ` + "- This session has its own Slate (working-set scratch, not project knowledge): " + "**slate_read** shows the Course (if a Flow is bound) and Seeds recorded so far \u2014 nothing " + "here is auto-injected, so call it if you want to see it. **slate_write_seed** with " + "`{ text, kind? }` records a draft hypothesis/decision/follow-up worth a later human " + "review. WRITE A SEED when you: found a root cause or a bug worth remembering; changed or " + "added code (summarize WHAT changed and WHY); took a design/architecture decision; " + "identified a risk; or discovered a constraint/lesson. Use the `kind` that fits: " + "`decision` (a choice made), `wiki-update` (something a wiki page should say), " + "`memory-entry` (a lesson/constraint), `follow-up` (a TODO for a later session), " + "`risk`, or `contract-change`. Keep each Seed to 2-3 sentences, concrete and specific. " + "Do NOT write Seeds for routine progress notes, one-shot operational requests (e.g. " + '"run git pull", "count files"), or trivia \u2014 those need no workspace and no proposal. ' + "Seeds are the ONLY input wrap-up proposes from: a session whose Slate has zero Seeds " + `produces zero proposals. A Seed is never accepted knowledge by itself.
29490
+ ` + "- For multi-step work, use **plan_set** to publish a structured plan, **plan_update** after each real status " + "change, and **plan_get** before resolving a revision conflict. Keep stable item ids, at most one " + "`in_progress` item, and do not mark work complete before verification. These tools update session metadata " + "only; `/plan` remains the operator's separate read-only permission mode.\n" + "- This session has its own Slate (working-set scratch, not project knowledge): " + "**slate_read** shows the Course (if a Flow is bound) and Seeds recorded so far \u2014 nothing " + "here is auto-injected, so call it if you want to see it. **slate_write_seed** with " + "`{ text, kind? }` records a draft hypothesis/decision/follow-up worth a later human " + "review. WRITE A SEED when you: found a root cause or a bug worth remembering; changed or " + "added code (summarize WHAT changed and WHY); took a design/architecture decision; " + "identified a risk; or discovered a constraint/lesson. Use the `kind` that fits: " + "`decision` (a choice made), `wiki-update` (something a wiki page should say), " + "`memory-entry` (a lesson/constraint), `follow-up` (a TODO for a later session), " + "`risk`, or `contract-change`. Keep each Seed to 2-3 sentences, concrete and specific. " + "Do NOT write Seeds for routine progress notes, one-shot operational requests (e.g. " + '"run git pull", "count files"), or trivia \u2014 those need no workspace and no proposal. ' + "Seeds are the ONLY input wrap-up proposes from: a session whose Slate has zero Seeds " + `produces zero proposals. A Seed is never accepted knowledge by itself.
29364
29491
  ` + "- Shared Agent Context (SAC) workspaces hold accepted, evidence-backed project context " + "beyond this codebase. **workspace_list** with `{ includeArchived? }` shows every workspace " + "visible to you \u2014 call it first when the user references a shared team workspace or accepted " + "project context, or before creating a new workspace, to judge whether an existing one " + "already fits the current topic. **workspace_show** with `{ workspaceId }` shows one " + "workspace's manifest. **workspace_overview** with `{ workspaceId }`, then **workspace_read** " + "with `{ workspaceId, itemId }` for one specific item, reads its accepted Facts/Work/Know-how. " + "**workspace_create** with `{ title, component? }` creates a new workspace AND binds it to " + "this session's slate (wrap-up then proposes into it) \u2014 only when workspace_list found no " + "fitting one and the session has real, durable results worth persisting; a workspace is " + "meant to persist across sessions, so prefer an existing one over creating another for the " + "same topic, and do NOT create one for one-shot operational requests. **workspace_propose** with " + "`{ workspaceId, kind, sessionId?, note? }` (sessionId defaults to this session) proposes a decision/wiki-update/memory-entry/" + "follow-up/contract-change/risk from this session for later human review \u2014 it never accepts " + "anything by itself; accepting always requires a human running `keryx workspace review` at a " + `real terminal, never this tool.
29365
29492
  ` + "- When you need a decision, interview step, or clarification: use **ask_user** with " + "2\u20136 options `{ id, label, description, recommended? }` (mark one recommended). " + `Do not dump long prose questions without options.
29366
29493
  ` + "- For a focused independent subtask (investigate X, review Y, research Z): use " + "**spawn_subagent** with `{ task, mode?: 'read_only'|'general', label? }`. " + "Default mode is read_only (no shell). Prefer spawn for work that can finish " + `without your intermediate turns; do not spawn for trivial one-line answers.
@@ -29657,6 +29784,7 @@ async function runAgentTurnCore(io, deps, history, userLine, options = {}) {
29657
29784
  return "stop";
29658
29785
  };
29659
29786
  let toollessReprompts = 0;
29787
+ let planFollowThroughUsed = false;
29660
29788
  let lastToollessText;
29661
29789
  for (;; ) {
29662
29790
  if (roundState.round >= roundState.maxRounds) {
@@ -29666,7 +29794,20 @@ async function runAgentTurnCore(io, deps, history, userLine, options = {}) {
29666
29794
  return { finishReason: "budget" };
29667
29795
  }
29668
29796
  roundState.round += 1;
29669
- const preRequestEstimate = estimateRequestTokens(history, deps.systemInstruction, toolDefs);
29797
+ let currentPlan;
29798
+ if (options.slateSession?.opened === true) {
29799
+ try {
29800
+ currentPlan = await getExecutionPlan(options.slateSession.dir);
29801
+ } catch (cause) {
29802
+ io.onSystem?.(`execution plan read failed (ignored): ${cause instanceof Error ? cause.message : String(cause)}
29803
+ `);
29804
+ }
29805
+ }
29806
+ const planSnapshot = renderExecutionPlanSnapshot(currentPlan);
29807
+ const roundSystemInstruction = planSnapshot === undefined ? deps.systemInstruction : `${deps.systemInstruction}
29808
+
29809
+ ${planSnapshot}`;
29810
+ const preRequestEstimate = estimateRequestTokens(history, roundSystemInstruction, toolDefs);
29670
29811
  if (needsCompaction(preRequestEstimate, deps.contextWindow)) {
29671
29812
  const compacted = compactMessages(history, { keepLastUserTurns: 3 });
29672
29813
  if (!compacted.noop) {
@@ -29681,7 +29822,7 @@ async function runAgentTurnCore(io, deps, history, userLine, options = {}) {
29681
29822
  const baseRequest = {
29682
29823
  providerId: deps.providerId,
29683
29824
  modelId: deps.modelId,
29684
- systemInstruction: deps.systemInstruction,
29825
+ systemInstruction: roundSystemInstruction,
29685
29826
  messages: [...history],
29686
29827
  tools: toolDefs,
29687
29828
  budget: { maxOutputTokens, runReservation: maxOutputTokens },
@@ -29911,6 +30052,32 @@ async function runAgentTurnCore(io, deps, history, userLine, options = {}) {
29911
30052
  }
29912
30053
  continue;
29913
30054
  }
30055
+ if (!planFollowThroughUsed && hasActionableExecutionPlanItems(currentPlan)) {
30056
+ planFollowThroughUsed = true;
30057
+ history.push({
30058
+ role: "user",
30059
+ content: "[system] The current execution plan still has actionable items remaining. Continue the work now. " + "Do not give another final reply until the plan is complete or genuinely blocked.",
30060
+ provenance: "project",
30061
+ ts: now()
30062
+ });
30063
+ io.onHistoryChange?.("tool");
30064
+ continue;
30065
+ }
30066
+ if (planFollowThroughUsed && hasActionableExecutionPlanItems(currentPlan)) {
30067
+ const actionable = currentPlan?.items.filter((item) => item.status === "pending" || item.status === "in_progress") ?? [];
30068
+ const shown = actionable.slice(0, 7).map((item) => {
30069
+ const title = item.title.length > 120 ? `${item.title.slice(0, 119)}\u2026` : item.title;
30070
+ return `- ${item.id} [${item.status}]: ${title}`;
30071
+ });
30072
+ if (actionable.length > shown.length) {
30073
+ shown.push(`- \u2026 ${actionable.length - shown.length} more actionable item(s)`);
30074
+ }
30075
+ system(`
30076
+ [plan] Actionable items remain after the single follow-through:
30077
+ ${shown.join(`
30078
+ `)}
30079
+ `);
30080
+ }
29914
30081
  return {};
29915
30082
  }
29916
30083
  if (isAborted()) {
@@ -30481,6 +30648,7 @@ var init_agent = __esm(() => {
30481
30648
  init_peer_notification();
30482
30649
  init_scheduler();
30483
30650
  init_slate();
30651
+ init_execution_plan();
30484
30652
  init_slate_course();
30485
30653
  init_machine_wrap_up();
30486
30654
  init_slate_lifecycle();
@@ -44782,7 +44950,7 @@ import { stdout } from "process";
44782
44950
  // src/lib/md-blocks.ts
44783
44951
  var FENCE_LINE = /^[ \t]{0,3}(```|~~~)(.*)\r?$/;
44784
44952
  var HUNK_HEADER = /^@@ -\d+(?:,\d+)? \+\d+(?:,\d+)? @@/;
44785
- var MARKDOWN_LANGS = new Set(["md", "markdown", "prompt", "txt", "text"]);
44953
+ var MARKDOWN_LANGS = new Set(["md", "markdown", "prompt"]);
44786
44954
  var DIFF_LANGS = new Set(["diff", "patch"]);
44787
44955
  function stripTrailingCr(line) {
44788
44956
  return line.endsWith("\r") ? line.slice(0, -1) : line;
@@ -44791,6 +44959,101 @@ function splitLines(text) {
44791
44959
  return text.split(`
44792
44960
  `).map(stripTrailingCr);
44793
44961
  }
44962
+ function markdownTableCells(line) {
44963
+ const source = line.trim();
44964
+ if (!source.includes("|")) {
44965
+ return;
44966
+ }
44967
+ const cells = [];
44968
+ let cell = "";
44969
+ let inCode = false;
44970
+ for (let index = 0;index < source.length; index += 1) {
44971
+ const char = source[index] ?? "";
44972
+ if (char === "\\" && source[index + 1] === "|") {
44973
+ cell += "|";
44974
+ index += 1;
44975
+ continue;
44976
+ }
44977
+ if (char === "`") {
44978
+ inCode = !inCode;
44979
+ cell += char;
44980
+ continue;
44981
+ }
44982
+ if (char === "|" && !inCode) {
44983
+ cells.push(cell.trim());
44984
+ cell = "";
44985
+ continue;
44986
+ }
44987
+ cell += char;
44988
+ }
44989
+ cells.push(cell.trim());
44990
+ if (source.startsWith("|")) {
44991
+ cells.shift();
44992
+ }
44993
+ if (source.endsWith("|")) {
44994
+ cells.pop();
44995
+ }
44996
+ return cells.length >= 2 ? cells : undefined;
44997
+ }
44998
+ function tableAlignments(line, columns) {
44999
+ const cells = markdownTableCells(line);
45000
+ if (cells === undefined || cells.length !== columns) {
45001
+ return;
45002
+ }
45003
+ const alignments = [];
45004
+ for (const cell of cells) {
45005
+ const marker = cell.trim();
45006
+ if (!/^:?-{3,}:?$/.test(marker)) {
45007
+ return;
45008
+ }
45009
+ alignments.push(marker.startsWith(":") && marker.endsWith(":") ? "center" : marker.endsWith(":") ? "right" : "left");
45010
+ }
45011
+ return alignments;
45012
+ }
45013
+ function parseMarkdownTables(text) {
45014
+ if (text.length === 0) {
45015
+ return [];
45016
+ }
45017
+ const lines = splitLines(text);
45018
+ const parts = [];
45019
+ let proseStart = 0;
45020
+ const flushProse = (end) => {
45021
+ if (end <= proseStart) {
45022
+ return;
45023
+ }
45024
+ const prose = lines.slice(proseStart, end).join(`
45025
+ `);
45026
+ if (prose.length > 0) {
45027
+ parts.push({ kind: "prose", text: prose });
45028
+ }
45029
+ };
45030
+ for (let index = 0;index + 1 < lines.length; index += 1) {
45031
+ const headers = markdownTableCells(lines[index] ?? "");
45032
+ if (headers === undefined) {
45033
+ continue;
45034
+ }
45035
+ const alignments = tableAlignments(lines[index + 1] ?? "", headers.length);
45036
+ if (alignments === undefined) {
45037
+ continue;
45038
+ }
45039
+ flushProse(index);
45040
+ const rows = [];
45041
+ let cursor = index + 2;
45042
+ while (cursor < lines.length) {
45043
+ const row = markdownTableCells(lines[cursor] ?? "");
45044
+ if (row === undefined || row.length !== headers.length) {
45045
+ break;
45046
+ }
45047
+ rows.push(row);
45048
+ cursor += 1;
45049
+ }
45050
+ parts.push({ kind: "table", table: { headers, alignments, rows } });
45051
+ proseStart = cursor;
45052
+ index = cursor - 1;
45053
+ }
45054
+ flushProse(lines.length);
45055
+ return parts.length > 0 ? parts : [{ kind: "prose", text }];
45056
+ }
44794
45057
  function fenceInfo(line) {
44795
45058
  const match = FENCE_LINE.exec(line);
44796
45059
  if (match === null) {
@@ -51141,7 +51404,7 @@ import path47 from "path";
51141
51404
  // package.json
51142
51405
  var package_default = {
51143
51406
  name: "@mrciphersmith/keryx",
51144
- version: "0.2.139",
51407
+ version: "0.2.143",
51145
51408
  description: "Version-controlled project context for AI coding agents: code graph, architecture wiki, project memory, relevant tests, quality signals, and task flows.",
51146
51409
  private: false,
51147
51410
  publishConfig: {
@@ -86842,6 +87105,107 @@ shell_exec: no output for ${info.idleTimeoutMs}ms, so the command was killed (ra
86842
87105
  };
86843
87106
  }
86844
87107
 
87108
+ // src/harness/tool/builtin/execution-plan-tool.ts
87109
+ init_execution_plan();
87110
+ var statusSchema = { type: "string", enum: [...EXECUTION_PLAN_STATUSES] };
87111
+ function noSession(name) {
87112
+ return { output: `${name}: no active session in this run`, isError: true };
87113
+ }
87114
+ function failed(name, cause) {
87115
+ return { output: `${name} failed: ${cause instanceof Error ? cause.message : String(cause)}`, isError: true };
87116
+ }
87117
+ function executionPlanTools(getSessionDir) {
87118
+ const get = {
87119
+ definition: {
87120
+ name: "plan_get",
87121
+ description: "Read the current structured execution plan. Input: {}.",
87122
+ inputSchema: { type: "object", properties: {}, additionalProperties: false },
87123
+ risk: "read"
87124
+ },
87125
+ invoke: async () => {
87126
+ const dir = getSessionDir();
87127
+ if (dir === undefined)
87128
+ return noSession("plan_get");
87129
+ try {
87130
+ return { output: JSON.stringify(await getExecutionPlan(dir) ?? null, null, 2), isError: false };
87131
+ } catch (cause) {
87132
+ return failed("plan_get", cause);
87133
+ }
87134
+ }
87135
+ };
87136
+ const set = {
87137
+ definition: {
87138
+ name: "plan_set",
87139
+ description: "Replace the structured execution plan using optimistic revision control.",
87140
+ inputSchema: {
87141
+ type: "object",
87142
+ properties: {
87143
+ expectedRevision: { type: "integer", minimum: 0 },
87144
+ items: {
87145
+ type: "array",
87146
+ items: {
87147
+ type: "object",
87148
+ properties: { id: { type: "string", minLength: 1 }, title: { type: "string", minLength: 1 }, status: statusSchema },
87149
+ required: ["id", "title", "status"],
87150
+ additionalProperties: false
87151
+ }
87152
+ }
87153
+ },
87154
+ required: ["expectedRevision", "items"],
87155
+ additionalProperties: false
87156
+ },
87157
+ risk: "read"
87158
+ },
87159
+ invoke: async (input) => {
87160
+ const dir = getSessionDir();
87161
+ if (dir === undefined)
87162
+ return noSession("plan_set");
87163
+ try {
87164
+ const plan = await setExecutionPlan(dir, {
87165
+ expectedRevision: input.expectedRevision,
87166
+ items: input.items
87167
+ });
87168
+ return { output: JSON.stringify(plan, null, 2), isError: false };
87169
+ } catch (cause) {
87170
+ return failed("plan_set", cause);
87171
+ }
87172
+ }
87173
+ };
87174
+ const update = {
87175
+ definition: {
87176
+ name: "plan_update",
87177
+ description: "Update one execution-plan item's status using optimistic revision control.",
87178
+ inputSchema: {
87179
+ type: "object",
87180
+ properties: {
87181
+ expectedRevision: { type: "integer", minimum: 0 },
87182
+ itemId: { type: "string", minLength: 1 },
87183
+ status: statusSchema
87184
+ },
87185
+ required: ["expectedRevision", "itemId", "status"],
87186
+ additionalProperties: false
87187
+ },
87188
+ risk: "read"
87189
+ },
87190
+ invoke: async (input) => {
87191
+ const dir = getSessionDir();
87192
+ if (dir === undefined)
87193
+ return noSession("plan_update");
87194
+ try {
87195
+ const plan = await updateExecutionPlan(dir, {
87196
+ expectedRevision: input.expectedRevision,
87197
+ itemId: input.itemId,
87198
+ status: input.status
87199
+ });
87200
+ return { output: JSON.stringify(plan, null, 2), isError: false };
87201
+ } catch (cause) {
87202
+ return failed("plan_update", cause);
87203
+ }
87204
+ }
87205
+ };
87206
+ return [set, update, get];
87207
+ }
87208
+
86845
87209
  // src/harness/tool/builtin/slate-tool.ts
86846
87210
  init_slate();
86847
87211
  init_slate_course();
@@ -87760,6 +88124,7 @@ function buildInteractiveAgentTools(input) {
87760
88124
  createAskUserTool(invokeAskUserHost),
87761
88125
  slateReadTool(input.cwd, getSessionDir),
87762
88126
  slateWriteSeedTool(getSessionDir, idSeq, clock),
88127
+ ...executionPlanTools(getSessionDir),
87763
88128
  ...input.bus?.client() === undefined ? [] : buildBusTools(input.bus.client),
87764
88129
  ...input.mcp === undefined ? [] : createMcpInteractiveTools({
87765
88130
  catalog: input.mcp.catalog,
@@ -91315,9 +91680,81 @@ class LiveMarkdownBlock {
91315
91680
  init_config_dir();
91316
91681
  import { existsSync as existsSync33 } from "fs";
91317
91682
  import path192 from "path";
91318
- var THEME_NAMES = ["groknight", "grokday", "tokyonight", "keryx"];
91683
+ function hexChannels(hex) {
91684
+ return [1, 3, 5].map((offset) => Number.parseInt(hex.slice(offset, offset + 2), 16));
91685
+ }
91686
+ function mixHex(base, accent, accentWeight) {
91687
+ const a = hexChannels(base);
91688
+ const b = hexChannels(accent);
91689
+ const channel = (index) => Math.round((a[index] ?? 0) * (1 - accentWeight) + (b[index] ?? 0) * accentWeight).toString(16).padStart(2, "0");
91690
+ return `#${channel(0)}${channel(1)}${channel(2)}`;
91691
+ }
91692
+ function relativeLuminance(hex) {
91693
+ const linear = hexChannels(hex).map((channel) => {
91694
+ const value = channel / 255;
91695
+ return value <= 0.04045 ? value / 12.92 : ((value + 0.055) / 1.055) ** 2.4;
91696
+ });
91697
+ return 0.2126 * (linear[0] ?? 0) + 0.7152 * (linear[1] ?? 0) + 0.0722 * (linear[2] ?? 0);
91698
+ }
91699
+ function contrastRatio(a, b) {
91700
+ const [light, dark] = [relativeLuminance(a), relativeLuminance(b)].sort((x, y) => y - x);
91701
+ return ((light ?? 0) + 0.05) / ((dark ?? 0) + 0.05);
91702
+ }
91703
+ function transcriptAccent(theme, accent) {
91704
+ for (let weight = 0.5;weight >= 0; weight -= 0.05) {
91705
+ const candidate = mixHex(theme.text, accent, weight);
91706
+ if (contrastRatio(candidate, theme.bg) >= 3 && contrastRatio(candidate, theme.panel) >= 3) {
91707
+ return candidate;
91708
+ }
91709
+ }
91710
+ return theme.text;
91711
+ }
91712
+ function deriveTranscriptPalette(theme) {
91713
+ const heading = transcriptAccent(theme, theme.assistant);
91714
+ const muted = transcriptAccent(theme, theme.muted);
91715
+ const positive = transcriptAccent(theme, theme.ok);
91716
+ const negative = transcriptAccent(theme, theme.error);
91717
+ const focus = transcriptAccent(theme, theme.focus);
91718
+ return {
91719
+ prose: theme.text,
91720
+ heading,
91721
+ emphasis: theme.text,
91722
+ inlineCode: focus,
91723
+ inlineCodeBackground: theme.highlight,
91724
+ blockMeta: theme.muted,
91725
+ tableHeader: heading,
91726
+ tableBorder: theme.border,
91727
+ codePlain: theme.text,
91728
+ codeComment: muted,
91729
+ codeString: positive,
91730
+ codeNumber: focus,
91731
+ codeKeyword: heading,
91732
+ diffContext: theme.text,
91733
+ diffMeta: muted,
91734
+ diffHunk: heading,
91735
+ diffAdd: positive,
91736
+ diffDelete: negative,
91737
+ diffAddBackground: mixHex(theme.panel, theme.ok, 0.12),
91738
+ diffDeleteBackground: mixHex(theme.panel, theme.error, 0.12)
91739
+ };
91740
+ }
91741
+ var THEME_NAMES = [
91742
+ "groknight",
91743
+ "tokyonight",
91744
+ "keryx",
91745
+ "midnight",
91746
+ "nord",
91747
+ "ember",
91748
+ "violet",
91749
+ "grokday",
91750
+ "paper",
91751
+ "frost",
91752
+ "sand",
91753
+ "mint"
91754
+ ];
91319
91755
  var THEME_IDS = ["auto", ...THEME_NAMES];
91320
91756
  var DEFAULT_THEME_ID = "groknight";
91757
+ var LIGHT_THEME_NAMES = new Set(["grokday", "paper", "frost", "sand", "mint"]);
91321
91758
  var THEMES = {
91322
91759
  groknight: {
91323
91760
  name: "groknight",
@@ -91358,7 +91795,7 @@ var THEMES = {
91358
91795
  highlight: "#292e42",
91359
91796
  border: "#3b4261",
91360
91797
  text: "#c0caf5",
91361
- muted: "#565f89",
91798
+ muted: "#737da8",
91362
91799
  user: "#a9b1d6",
91363
91800
  assistant: "#bb9af7",
91364
91801
  tool: "#7aa2f7",
@@ -91382,11 +91819,139 @@ var THEMES = {
91382
91819
  focus: "#ffd166",
91383
91820
  error: "#e05a5a",
91384
91821
  ok: "#6bcf6b"
91822
+ },
91823
+ midnight: {
91824
+ name: "midnight",
91825
+ bg: "#080d18",
91826
+ panel: "#101827",
91827
+ highlight: "#17233a",
91828
+ border: "#40577a",
91829
+ text: "#dbe7ff",
91830
+ muted: "#8292ad",
91831
+ user: "#a9c7ff",
91832
+ assistant: "#8bd5ca",
91833
+ tool: "#7aa2f7",
91834
+ side: "#c6a0f6",
91835
+ focus: "#f5c26b",
91836
+ error: "#ff7a90",
91837
+ ok: "#83d17f"
91838
+ },
91839
+ nord: {
91840
+ name: "nord",
91841
+ bg: "#242933",
91842
+ panel: "#2e3440",
91843
+ highlight: "#3b4252",
91844
+ border: "#5e6b80",
91845
+ text: "#eceff4",
91846
+ muted: "#929db0",
91847
+ user: "#d8dee9",
91848
+ assistant: "#88c0d0",
91849
+ tool: "#81a1c1",
91850
+ side: "#b48ead",
91851
+ focus: "#ebcb8b",
91852
+ error: "#f0808b",
91853
+ ok: "#a3be8c"
91854
+ },
91855
+ ember: {
91856
+ name: "ember",
91857
+ bg: "#17100d",
91858
+ panel: "#211713",
91859
+ highlight: "#30201a",
91860
+ border: "#6a4d3d",
91861
+ text: "#f2e3d5",
91862
+ muted: "#a48672",
91863
+ user: "#ffd2ad",
91864
+ assistant: "#ff9f68",
91865
+ tool: "#e1b274",
91866
+ side: "#d89b72",
91867
+ focus: "#ffd166",
91868
+ error: "#ff7676",
91869
+ ok: "#a6d27a"
91870
+ },
91871
+ violet: {
91872
+ name: "violet",
91873
+ bg: "#100b1b",
91874
+ panel: "#1a1227",
91875
+ highlight: "#281b3c",
91876
+ border: "#61497d",
91877
+ text: "#eee8ff",
91878
+ muted: "#9f8db8",
91879
+ user: "#d6c4ff",
91880
+ assistant: "#c099ff",
91881
+ tool: "#76b7ff",
91882
+ side: "#ff8cc6",
91883
+ focus: "#ffd166",
91884
+ error: "#ff7699",
91885
+ ok: "#7bdba5"
91886
+ },
91887
+ paper: {
91888
+ name: "paper",
91889
+ bg: "#faf7f0",
91890
+ panel: "#f1ece1",
91891
+ highlight: "#e5ddcf",
91892
+ border: "#9d9280",
91893
+ text: "#29251f",
91894
+ muted: "#746d62",
91895
+ user: "#4b4033",
91896
+ assistant: "#6b4bbc",
91897
+ tool: "#1f62a8",
91898
+ side: "#8a4f72",
91899
+ focus: "#815500",
91900
+ error: "#b4233c",
91901
+ ok: "#2e7d32"
91902
+ },
91903
+ frost: {
91904
+ name: "frost",
91905
+ bg: "#f4f8fb",
91906
+ panel: "#eaf1f6",
91907
+ highlight: "#dbe8f0",
91908
+ border: "#93a8b7",
91909
+ text: "#1f2d3a",
91910
+ muted: "#627483",
91911
+ user: "#314c65",
91912
+ assistant: "#5b4bb7",
91913
+ tool: "#1769aa",
91914
+ side: "#794ca8",
91915
+ focus: "#805400",
91916
+ error: "#b32642",
91917
+ ok: "#2d7544"
91918
+ },
91919
+ sand: {
91920
+ name: "sand",
91921
+ bg: "#fff7e8",
91922
+ panel: "#f6ead5",
91923
+ highlight: "#ead9bb",
91924
+ border: "#ad936d",
91925
+ text: "#33291d",
91926
+ muted: "#776751",
91927
+ user: "#5b4630",
91928
+ assistant: "#744494",
91929
+ tool: "#176b75",
91930
+ side: "#91483b",
91931
+ focus: "#805400",
91932
+ error: "#b3263b",
91933
+ ok: "#347536"
91934
+ },
91935
+ mint: {
91936
+ name: "mint",
91937
+ bg: "#f2faf7",
91938
+ panel: "#e4f3ed",
91939
+ highlight: "#d3e9e0",
91940
+ border: "#8fafa3",
91941
+ text: "#19332a",
91942
+ muted: "#5d776d",
91943
+ user: "#285548",
91944
+ assistant: "#6745a6",
91945
+ tool: "#176b72",
91946
+ side: "#86506b",
91947
+ focus: "#805400",
91948
+ error: "#b32643",
91949
+ ok: "#217343"
91385
91950
  }
91386
91951
  };
91387
91952
  var kind = DEFAULT_THEME_ID;
91388
91953
  var resolved = THEMES.groknight;
91389
- var listeners = new Set;
91954
+ var listeners2 = new Set;
91390
91955
  function parseThemeId(raw) {
91391
91956
  const key = raw.trim().toLowerCase();
91392
91957
  if (key === "auto" || key === "system") {
@@ -91419,28 +91984,22 @@ function themeLabel(id) {
91419
91984
  switch (id) {
91420
91985
  case "auto":
91421
91986
  return "auto (follow terminal)";
91422
- case "groknight":
91423
- return "groknight";
91424
- case "grokday":
91425
- return "grokday";
91426
- case "tokyonight":
91427
- return "tokyonight";
91428
- case "keryx":
91429
- return "keryx";
91987
+ default:
91988
+ return LIGHT_THEME_NAMES.has(id) ? `${id} \u2600` : id;
91430
91989
  }
91431
91990
  }
91432
91991
  function applyThemeId(id, mode = null) {
91433
91992
  kind = id;
91434
91993
  resolved = resolveTheme(id, mode);
91435
- for (const listener of listeners) {
91994
+ for (const listener of listeners2) {
91436
91995
  listener(resolved, kind);
91437
91996
  }
91438
91997
  return resolved;
91439
91998
  }
91440
91999
  function onThemeChange(listener) {
91441
- listeners.add(listener);
92000
+ listeners2.add(listener);
91442
92001
  return () => {
91443
- listeners.delete(listener);
92002
+ listeners2.delete(listener);
91444
92003
  };
91445
92004
  }
91446
92005
  function tuiConfigPath(dir) {
@@ -92169,11 +92728,15 @@ function clipBody(text, maxLines = MAX_BODY_LINES) {
92169
92728
  return [...lines.slice(0, maxLines), `\u2026 (${hidden} more line${hidden === 1 ? "" : "s"} not shown)`].join(`
92170
92729
  `);
92171
92730
  }
92172
- function markdownToChunks(otui, md) {
92731
+ function themedChunk(otui, text, fg, bg) {
92732
+ const foreground = otui.fg(fg)(text);
92733
+ return bg === undefined ? foreground : otui.bg(bg)(foreground);
92734
+ }
92735
+ function markdownToChunks(otui, md, palette = deriveTranscriptPalette(getTheme())) {
92173
92736
  const out = [];
92174
92737
  const plain = (s) => {
92175
92738
  if (s.length > 0) {
92176
- out.push(...otui.stringToStyledText(s).chunks);
92739
+ out.push(themedChunk(otui, s, palette.prose));
92177
92740
  }
92178
92741
  };
92179
92742
  const inline = (text) => {
@@ -92183,9 +92746,9 @@ function markdownToChunks(otui, md) {
92183
92746
  while ((m = re.exec(text)) !== null) {
92184
92747
  plain(text.slice(last, m.index));
92185
92748
  if (m[1] !== undefined) {
92186
- out.push(otui.dim(m[1].slice(1, -1)));
92749
+ out.push(themedChunk(otui, m[1].slice(1, -1), palette.inlineCode, palette.inlineCodeBackground));
92187
92750
  } else if (m[2] !== undefined) {
92188
- out.push(otui.bold(m[2].slice(2, -2)));
92751
+ out.push(otui.bold(themedChunk(otui, m[2].slice(2, -2), palette.emphasis)));
92189
92752
  }
92190
92753
  last = m.index + m[0].length;
92191
92754
  }
@@ -92204,12 +92767,12 @@ function markdownToChunks(otui, md) {
92204
92767
  `);
92205
92768
  }
92206
92769
  if (inCode) {
92207
- out.push(otui.dim(line));
92770
+ out.push(themedChunk(otui, line, palette.codePlain));
92208
92771
  continue;
92209
92772
  }
92210
92773
  const heading = /^#{1,6}\s+(.*)$/.exec(line);
92211
92774
  if (heading !== null) {
92212
- out.push(otui.cyan(otui.bold(heading[1] ?? "")));
92775
+ out.push(otui.bold(themedChunk(otui, heading[1] ?? "", palette.heading)));
92213
92776
  continue;
92214
92777
  }
92215
92778
  const bullet = /^(\s*)[-*]\s+(.*)$/.exec(line);
@@ -92222,109 +92785,150 @@ function markdownToChunks(otui, md) {
92222
92785
  }
92223
92786
  return out;
92224
92787
  }
92225
- function diffChunks(otui, text) {
92788
+ function diffChunks(otui, text, palette = deriveTranscriptPalette(getTheme())) {
92226
92789
  const out = [];
92227
92790
  const lines = splitLines(text);
92228
92791
  for (const [index, line] of lines.entries()) {
92229
92792
  if (index > 0) {
92230
- out.push(...otui.stringToStyledText(`
92231
- `).chunks);
92793
+ out.push(themedChunk(otui, `
92794
+ `, palette.diffContext));
92232
92795
  }
92233
92796
  switch (classifyDiffLine(line)) {
92234
92797
  case "add":
92235
- out.push(otui.green(line));
92798
+ out.push(themedChunk(otui, line, palette.diffAdd, palette.diffAddBackground));
92236
92799
  break;
92237
92800
  case "del":
92238
- out.push(otui.red(line));
92801
+ out.push(themedChunk(otui, line, palette.diffDelete, palette.diffDeleteBackground));
92239
92802
  break;
92240
92803
  case "hunk":
92241
- out.push(otui.cyan(line));
92804
+ out.push(themedChunk(otui, line, palette.diffHunk));
92242
92805
  break;
92243
92806
  case "meta":
92244
- out.push(otui.dim(line));
92807
+ out.push(themedChunk(otui, line, palette.diffMeta));
92245
92808
  break;
92246
92809
  default:
92247
- out.push(...otui.stringToStyledText(line).chunks);
92810
+ out.push(themedChunk(otui, line, palette.diffContext));
92248
92811
  }
92249
92812
  }
92250
92813
  return out;
92251
92814
  }
92252
- function flatDimChunks(otui, text) {
92815
+ function flatDimChunks(otui, text, palette = deriveTranscriptPalette(getTheme())) {
92253
92816
  const out = [];
92254
92817
  for (const [index, line] of splitLines(text).entries()) {
92255
92818
  if (index > 0) {
92256
- out.push(...otui.stringToStyledText(`
92257
- `).chunks);
92819
+ out.push(otui.dim(themedChunk(otui, `
92820
+ `, palette.blockMeta)));
92258
92821
  }
92259
- out.push(otui.dim(line));
92822
+ out.push(otui.dim(themedChunk(otui, line, palette.blockMeta)));
92260
92823
  }
92261
92824
  return out;
92262
92825
  }
92263
- function codeChunks(otui, text, lang) {
92826
+ function codeChunks(otui, text, lang, palette) {
92264
92827
  const out = [];
92265
92828
  for (const [index, line] of splitLines(text).entries()) {
92266
92829
  if (index > 0) {
92267
- out.push(...otui.stringToStyledText(`
92268
- `).chunks);
92830
+ out.push(themedChunk(otui, `
92831
+ `, palette.codePlain));
92269
92832
  }
92270
92833
  for (const token of tokenizeCodeLine(line, lang)) {
92271
92834
  switch (token.kind) {
92272
92835
  case "comment":
92273
- out.push(otui.dim(token.text));
92836
+ out.push(themedChunk(otui, token.text, palette.codeComment));
92274
92837
  break;
92275
92838
  case "string":
92276
- out.push(otui.green(token.text));
92839
+ out.push(themedChunk(otui, token.text, palette.codeString));
92277
92840
  break;
92278
92841
  case "number":
92279
- out.push(otui.yellow(token.text));
92842
+ out.push(themedChunk(otui, token.text, palette.codeNumber));
92280
92843
  break;
92281
92844
  case "keyword":
92282
- out.push(otui.cyan(token.text));
92845
+ out.push(themedChunk(otui, token.text, palette.codeKeyword));
92283
92846
  break;
92284
92847
  default:
92285
- out.push(...otui.stringToStyledText(token.text).chunks);
92848
+ out.push(themedChunk(otui, token.text, palette.codePlain));
92286
92849
  }
92287
92850
  }
92288
92851
  }
92289
92852
  return out;
92290
92853
  }
92291
- function payloadChunks(otui, text, lang = "") {
92854
+ function payloadChunks(otui, text, lang = "", palette = deriveTranscriptPalette(getTheme())) {
92292
92855
  const kind = payloadKind(lang, lineCountOf(text));
92293
92856
  if (kind === "diff" || looksLikeUnifiedDiff(text)) {
92294
- return diffChunks(otui, text);
92857
+ return diffChunks(otui, text, palette);
92295
92858
  }
92296
92859
  if (kind === "markdown" || lang.length === 0) {
92297
- return markdownToChunks(otui, text);
92860
+ return markdownToChunks(otui, text, palette);
92298
92861
  }
92299
- return codeChunks(otui, text, lang);
92862
+ return codeChunks(otui, text, lang, palette);
92300
92863
  }
92301
92864
  function createSegmentView(otui, renderer, parent, segment) {
92302
92865
  viewSeq += 1;
92303
92866
  const id = `seg${viewSeq}`;
92304
92867
  if (segment.kind === "text") {
92305
- const text = new otui.TextRenderable(renderer, {
92306
- id,
92307
- content: new otui.StyledText(markdownToChunks(otui, segment.text))
92308
- });
92309
- parent.add(text);
92868
+ let mounted = [];
92869
+ const paint = (source) => {
92870
+ for (const child of mounted) {
92871
+ try {
92872
+ parent.remove(child);
92873
+ child.destroyRecursively();
92874
+ } catch {}
92875
+ }
92876
+ mounted = [];
92877
+ const palette = deriveTranscriptPalette(getTheme());
92878
+ for (const [index, part] of parseMarkdownTables(source).entries()) {
92879
+ if (part.kind === "prose") {
92880
+ const text = new otui.TextRenderable(renderer, {
92881
+ id: `${id}-prose-${index}`,
92882
+ content: new otui.StyledText(markdownToChunks(otui, part.text, palette))
92883
+ });
92884
+ parent.add(text);
92885
+ mounted.push(text);
92886
+ continue;
92887
+ }
92888
+ const header = part.table.headers.map((cell) => [otui.bold(themedChunk(otui, cell, palette.tableHeader))]);
92889
+ const rows = part.table.rows.map((row) => row.map((cell) => markdownToChunks(otui, cell, palette)));
92890
+ const table = new otui.TextTableRenderable(renderer, {
92891
+ id: `${id}-table-${index}`,
92892
+ content: [header, ...rows],
92893
+ width: "100%",
92894
+ wrapMode: "word",
92895
+ columnWidthMode: "full",
92896
+ columnFitter: "balanced",
92897
+ cellPaddingX: 1,
92898
+ cellPaddingY: 0,
92899
+ showBorders: true,
92900
+ outerBorder: true,
92901
+ borderStyle: "rounded",
92902
+ borderColor: palette.tableBorder,
92903
+ fg: palette.prose,
92904
+ backgroundColor: getTheme().panel
92905
+ });
92906
+ parent.add(table);
92907
+ mounted.push(table);
92908
+ }
92909
+ };
92910
+ paint(segment.text);
92310
92911
  return {
92311
92912
  kind: "text",
92312
92913
  update: (next) => {
92313
92914
  if (next.kind === "text") {
92314
- text.content = new otui.StyledText(markdownToChunks(otui, next.text));
92915
+ paint(next.text);
92315
92916
  }
92316
92917
  },
92317
92918
  destroy: () => {
92318
- try {
92319
- parent.remove(text);
92320
- text.destroyRecursively();
92321
- } catch {}
92919
+ for (const child of mounted) {
92920
+ try {
92921
+ parent.remove(child);
92922
+ child.destroyRecursively();
92923
+ } catch {}
92924
+ }
92925
+ mounted = [];
92322
92926
  }
92323
92927
  };
92324
92928
  }
92325
92929
  const tag = (lang, body) => {
92326
92930
  const n = lineCountOf(body);
92327
- return `${lang.length > 0 ? lang : "text"} \xB7 ${n} ${n === 1 ? "line" : "lines"} \xB7 y copy`;
92931
+ return `${lang.length > 0 ? lang : "text"} \xB7 ${n} ${n === 1 ? "line" : "lines"} \xB7 ctrl+o then y copy`;
92328
92932
  };
92329
92933
  const frameWidth = (lang, body) => Math.max(hugWidth(body, FRAME_CHROME), hugWidth(tag(lang, body), FRAME_CHROME));
92330
92934
  const frame = new otui.BoxRenderable(renderer, {
@@ -92335,12 +92939,15 @@ function createSegmentView(otui, renderer, parent, segment) {
92335
92939
  borderStyle: "rounded",
92336
92940
  border: true,
92337
92941
  borderColor: frameColor(),
92942
+ backgroundColor: getTheme().panel,
92338
92943
  paddingLeft: 1,
92339
92944
  paddingRight: 1
92340
92945
  });
92341
92946
  const header = new otui.TextRenderable(renderer, {
92342
92947
  id: `${id}-tag`,
92343
- content: otui.t`${otui.dim(tag(segment.lang, segment.body))}`
92948
+ content: new otui.StyledText([
92949
+ themedChunk(otui, tag(segment.lang, segment.body), deriveTranscriptPalette(getTheme()).blockMeta)
92950
+ ])
92344
92951
  });
92345
92952
  const body = new otui.TextRenderable(renderer, {
92346
92953
  id: `${id}-body`,
@@ -92355,7 +92962,9 @@ function createSegmentView(otui, renderer, parent, segment) {
92355
92962
  if (next.kind !== "code") {
92356
92963
  return;
92357
92964
  }
92358
- header.content = otui.t`${otui.dim(tag(next.lang, next.body))}`;
92965
+ header.content = new otui.StyledText([
92966
+ themedChunk(otui, tag(next.lang, next.body), deriveTranscriptPalette(getTheme()).blockMeta)
92967
+ ]);
92359
92968
  body.content = new otui.StyledText(payloadChunks(otui, next.body, next.lang));
92360
92969
  frame.maxWidth = frameWidth(next.lang, next.body);
92361
92970
  },
@@ -92902,6 +93511,7 @@ function innerWidthOf(state) {
92902
93511
  }
92903
93512
  function paintTabs(state) {
92904
93513
  clearTranscriptChildren(state.tabStrip);
93514
+ const theme = getTheme();
92905
93515
  for (const [index, tab] of state.tabs.entries()) {
92906
93516
  const active = tab.id === state.active;
92907
93517
  const label = active ? `[${tab.label}]` : ` ${tab.label} `;
@@ -92909,6 +93519,7 @@ function paintTabs(state) {
92909
93519
  state.tabStrip.add(new state.otui.TextRenderable(state.chrome.renderer, {
92910
93520
  id: `modal-tab-${tab.id}`,
92911
93521
  content: active ? state.otui.t`${prefix}${state.otui.bold(label)}` : state.otui.t`${prefix}${state.otui.dim(label)}`,
93522
+ fg: active ? theme.focus : theme.muted,
92912
93523
  onMouseDown: () => {
92913
93524
  if (!state.open || state.input === undefined || tab.id === state.active) {
92914
93525
  return;
@@ -93017,12 +93628,14 @@ function ensureHost(otui, chrome) {
93017
93628
  const titleText = new otui.TextRenderable(r, {
93018
93629
  id: "modal-title",
93019
93630
  content: "",
93020
- flexGrow: 1
93631
+ flexGrow: 1,
93632
+ fg: getTheme().text
93021
93633
  });
93022
93634
  const closeText = new otui.TextRenderable(r, {
93023
93635
  id: "modal-close",
93024
93636
  content: "",
93025
93637
  flexShrink: 0,
93638
+ fg: getTheme().muted,
93026
93639
  onMouseDown: () => {
93027
93640
  const current = hosts.get(r);
93028
93641
  if (current !== undefined && current.open) {
@@ -93059,7 +93672,8 @@ function ensureHost(otui, chrome) {
93059
93672
  });
93060
93673
  const footerText = new otui.TextRenderable(r, {
93061
93674
  id: "modal-footer-text",
93062
- content: ""
93675
+ content: "",
93676
+ fg: getTheme().text
93063
93677
  });
93064
93678
  footer.add(footerText);
93065
93679
  panel.add(header);
@@ -93161,6 +93775,10 @@ function ensureHost(otui, chrome) {
93161
93775
  state.backdrop.backgroundColor = theme.bg;
93162
93776
  state.panel.backgroundColor = theme.panel;
93163
93777
  state.panel.borderColor = theme.border;
93778
+ state.titleText.fg = theme.text;
93779
+ state.closeText.fg = theme.muted;
93780
+ state.footerText.fg = theme.text;
93781
+ paintTabs(state);
93164
93782
  });
93165
93783
  return state;
93166
93784
  }
@@ -94962,12 +95580,50 @@ function themeColorRemap(from, to) {
94962
95580
  remap.set(oldColor, newColor);
94963
95581
  }
94964
95582
  }
95583
+ const fromTranscript = deriveTranscriptPalette(from);
95584
+ const toTranscript = deriveTranscriptPalette(to);
95585
+ for (const slot of Object.keys(fromTranscript)) {
95586
+ const oldColor = fromTranscript[slot];
95587
+ const newColor = toTranscript[slot];
95588
+ if (oldColor !== newColor) {
95589
+ remap.set(oldColor, newColor);
95590
+ }
95591
+ }
94965
95592
  return remap;
94966
95593
  }
94967
- function recolorThemeTree(node, remap) {
95594
+ function recolorContent(value, remap, color) {
95595
+ if (Array.isArray(value)) {
95596
+ let changed = false;
95597
+ for (const item of value) {
95598
+ changed = recolorContent(item, remap, color) || changed;
95599
+ }
95600
+ return changed;
95601
+ }
95602
+ if (value === null || typeof value !== "object") {
95603
+ return false;
95604
+ }
95605
+ const target = value;
95606
+ let changed = false;
95607
+ if (typeof target.text === "string") {
95608
+ for (const prop of ["fg", "bg"]) {
95609
+ const hex = themeColorToHex(target[prop]);
95610
+ const next = hex === undefined ? undefined : remap.get(hex);
95611
+ if (next !== undefined) {
95612
+ target[prop] = color(next);
95613
+ changed = true;
95614
+ }
95615
+ }
95616
+ }
95617
+ if (target.chunks !== undefined) {
95618
+ changed = recolorContent(target.chunks, remap, color) || changed;
95619
+ }
95620
+ return changed;
95621
+ }
95622
+ function recolorThemeTree(node, fromTheme, toTheme, color = (hex) => hex) {
94968
95623
  if (node === null || node === undefined) {
94969
95624
  return;
94970
95625
  }
95626
+ const remap = themeColorRemap(fromTheme, toTheme);
94971
95627
  const target = node;
94972
95628
  for (const prop of ["borderColor", "backgroundColor", "fg"]) {
94973
95629
  const hex = themeColorToHex(target[prop]);
@@ -94978,9 +95634,13 @@ function recolorThemeTree(node, remap) {
94978
95634
  }
94979
95635
  }
94980
95636
  }
95637
+ const content = target.content;
95638
+ if (content !== undefined && recolorContent(content, remap, color)) {
95639
+ target.content = content;
95640
+ }
94981
95641
  if (typeof target.getChildren === "function") {
94982
95642
  for (const child of target.getChildren()) {
94983
- recolorThemeTree(child, remap);
95643
+ recolorThemeTree(child, fromTheme, toTheme, color);
94984
95644
  }
94985
95645
  }
94986
95646
  }
@@ -95600,7 +96260,6 @@ async function createShellChrome(otui, renderer, opts) {
95600
96260
  }
95601
96261
  });
95602
96262
  const applyTheme = (theme = getTheme()) => {
95603
- const remap = themeColorRemap(appliedTheme, theme);
95604
96263
  try {
95605
96264
  r.setBackgroundColor(theme.bg);
95606
96265
  } catch {}
@@ -95625,14 +96284,15 @@ async function createShellChrome(otui, renderer, opts) {
95625
96284
  menu.selectedTextColor = theme.focus;
95626
96285
  menu.descriptionColor = theme.muted;
95627
96286
  menu.selectedDescriptionColor = theme.muted;
95628
- recolorThemeTree(transcript, remap);
95629
- recolorThemeTree(dock, remap);
95630
- recolorThemeTree(queueDock, remap);
95631
- recolorThemeTree(sidebarTop, remap);
95632
- recolorThemeTree(menu, remap);
95633
- recolorThemeTree(composer, remap);
95634
- recolorThemeTree(header, remap);
95635
- recolorThemeTree(footer, remap);
96287
+ const chunkColor = (hex) => otui.RGBA.fromHex(hex);
96288
+ recolorThemeTree(transcript, appliedTheme, theme, chunkColor);
96289
+ recolorThemeTree(dock, appliedTheme, theme, chunkColor);
96290
+ recolorThemeTree(queueDock, appliedTheme, theme, chunkColor);
96291
+ recolorThemeTree(sidebarTop, appliedTheme, theme, chunkColor);
96292
+ recolorThemeTree(menu, appliedTheme, theme, chunkColor);
96293
+ recolorThemeTree(composer, appliedTheme, theme, chunkColor);
96294
+ recolorThemeTree(header, appliedTheme, theme, chunkColor);
96295
+ recolorThemeTree(footer, appliedTheme, theme, chunkColor);
95636
96296
  appliedTheme = theme;
95637
96297
  };
95638
96298
  const unsubTheme = onThemeChange((theme) => applyTheme(theme));
@@ -102218,6 +102878,96 @@ function paintBackgroundJobSidebar(otui, renderer, parent, entries, options) {
102218
102878
  }
102219
102879
  }
102220
102880
 
102881
+ // src/tui/execution-plan-panel.ts
102882
+ init_execution_plan();
102883
+ var GLYPHS = {
102884
+ completed: "\u2713",
102885
+ in_progress: "\u25B6",
102886
+ pending: "\u25CB",
102887
+ blocked: "!",
102888
+ skipped: "\u2212"
102889
+ };
102890
+ function truncate7(text, width) {
102891
+ if (width <= 0)
102892
+ return "";
102893
+ if (displayWidth(text) <= width)
102894
+ return text;
102895
+ if (width === 1)
102896
+ return "\u2026";
102897
+ let prefix = "";
102898
+ for (const character of text) {
102899
+ if (displayWidth(prefix + character) > width - 1)
102900
+ break;
102901
+ prefix += character;
102902
+ }
102903
+ return `${prefix}\u2026`;
102904
+ }
102905
+ function projectExecutionPlanPanel(plan, options) {
102906
+ if (plan === undefined || plan.items.length === 0) {
102907
+ return { visible: false, rows: [], revision: undefined };
102908
+ }
102909
+ const size = Math.max(1, Math.min(options.maxRows ?? 7, 7, plan.items.length));
102910
+ const active = Math.max(0, plan.items.findIndex((item) => item.status === "in_progress"));
102911
+ const start = Math.max(0, Math.min(active - Math.floor(size / 2), plan.items.length - size));
102912
+ const rows = plan.items.slice(start, start + size).map((item) => {
102913
+ const glyph = GLYPHS[item.status];
102914
+ return { id: item.id, status: item.status, glyph, text: truncate7(`${glyph} ${item.title}`, options.width) };
102915
+ });
102916
+ return { visible: true, rows, revision: plan.revision };
102917
+ }
102918
+ function mountExecutionPlanPanel(otui, renderer, parent, options) {
102919
+ const box = parent;
102920
+ let paintedDir;
102921
+ let paintedRevision;
102922
+ let refreshGeneration = 0;
102923
+ let disposed = false;
102924
+ const paint = (plan, dir) => {
102925
+ const projected = projectExecutionPlanPanel(plan, {
102926
+ width: options.width,
102927
+ ...options.maxRows === undefined ? {} : { maxRows: options.maxRows }
102928
+ });
102929
+ if (paintedDir === dir && paintedRevision === projected.revision)
102930
+ return;
102931
+ paintedDir = dir;
102932
+ paintedRevision = projected.revision;
102933
+ if (box.getChildren !== undefined && box.remove !== undefined) {
102934
+ for (const child of [...box.getChildren()])
102935
+ box.remove(child);
102936
+ }
102937
+ const Ctor = otui.TextRenderable;
102938
+ if (!projected.visible || box.add === undefined || Ctor === undefined)
102939
+ return;
102940
+ box.add(new Ctor(renderer, { id: "sb-plan-h", content: "Plan", marginTop: 1 }));
102941
+ for (const row of projected.rows)
102942
+ box.add(new Ctor(renderer, { id: `sb-plan-${row.id}`, content: row.text }));
102943
+ };
102944
+ const refresh = async () => {
102945
+ const dir = options.getSessionDir();
102946
+ const generation = ++refreshGeneration;
102947
+ const plan = dir === undefined ? undefined : await getExecutionPlan(dir).catch(() => {
102948
+ return;
102949
+ });
102950
+ if (disposed || generation !== refreshGeneration || dir !== options.getSessionDir())
102951
+ return;
102952
+ paint(plan, dir);
102953
+ };
102954
+ const unsubscribe = subscribeExecutionPlans((dir, plan) => {
102955
+ if (dir === options.getSessionDir()) {
102956
+ refreshGeneration += 1;
102957
+ paint(plan, dir);
102958
+ }
102959
+ });
102960
+ refresh();
102961
+ return {
102962
+ refresh,
102963
+ dispose: () => {
102964
+ disposed = true;
102965
+ refreshGeneration += 1;
102966
+ unsubscribe();
102967
+ }
102968
+ };
102969
+ }
102970
+
102221
102971
  // src/tui/bus-command.ts
102222
102972
  var SEND_USAGE = "usage: /bus send @<name> <text> (or /bus @<name> <text>)";
102223
102973
  var ASK_USAGE = "usage: /bus ask @<name> <text>";
@@ -104201,6 +104951,7 @@ async function launchTuiAgentShell(opts) {
104201
104951
  let mountedChrome;
104202
104952
  let liveDeps;
104203
104953
  let liveJobs;
104954
+ let disposeExecutionPlanPanel;
104204
104955
  let detachExternal;
104205
104956
  let liveBus;
104206
104957
  let destroyed = false;
@@ -104223,6 +104974,7 @@ async function launchTuiAgentShell(opts) {
104223
104974
  try {
104224
104975
  const r = renderer = await createShellRenderer(otui, {
104225
104976
  onDestroy: () => {
104977
+ disposeExecutionPlanPanel?.();
104226
104978
  destroyed = true;
104227
104979
  foregroundOperation.cancel("renderer destroyed");
104228
104980
  foregroundOperation.dispose();
@@ -104262,6 +105014,7 @@ async function launchTuiAgentShell(opts) {
104262
105014
  saveShellConfig(sel.baseUrl === undefined ? { provider: sel.provider, model: sel.model } : { provider: sel.provider, model: sel.model, baseUrl: sel.baseUrl });
104263
105015
  let currentSel = sel;
104264
105016
  const suggestionGate = new NextStepSuggestionGate;
105017
+ let slateSession;
104265
105018
  const liveSlateSession = () => whilePersisting(slateSession, () => sessionLease.canPersist());
104266
105019
  let deps = await opts.makeAgentDeps(sel, liveSlateSession, busClientRef);
104267
105020
  liveDeps = deps;
@@ -104405,6 +105158,18 @@ async function launchTuiAgentShell(opts) {
104405
105158
  marginTop: 1
104406
105159
  });
104407
105160
  sidebar.add(sbSubagents);
105161
+ const sbPlan = new otui.BoxRenderable(r, {
105162
+ id: "sb-plan",
105163
+ flexDirection: "column",
105164
+ flexShrink: 0
105165
+ });
105166
+ sidebar.add(sbPlan);
105167
+ const executionPlanPanel = mountExecutionPlanPanel(otui, r, sbPlan, {
105168
+ getSessionDir: () => liveSlateSession()?.dir,
105169
+ width: SIDEBAR_TEXT_WIDTH,
105170
+ maxRows: 7
105171
+ });
105172
+ disposeExecutionPlanPanel = executionPlanPanel.dispose;
104408
105173
  const sbJobs = new otui.BoxRenderable(r, {
104409
105174
  id: "sb-jobs",
104410
105175
  flexDirection: "column",
@@ -104989,9 +105754,9 @@ async function launchTuiAgentShell(opts) {
104989
105754
  let archive = [];
104990
105755
  let nextArchiveIndex = 0;
104991
105756
  let sessionPersistTimer;
104992
- let slateSession;
104993
105757
  const bindSlateToLiveSession = () => {
104994
105758
  slateSession = freshSlateSessionRef(liveSession.dir, sessionCwd);
105759
+ executionPlanPanel.refresh();
104995
105760
  };
104996
105761
  const onContextCompaction = (r) => {
104997
105762
  if (!sessionLease.canPersist()) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mrciphersmith/keryx",
3
- "version": "0.2.139",
3
+ "version": "0.2.143",
4
4
  "description": "Version-controlled project context for AI coding agents: code graph, architecture wiki, project memory, relevant tests, quality signals, and task flows.",
5
5
  "private": false,
6
6
  "publishConfig": {