open-research 1.0.1 → 1.1.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 (2) hide show
  1. package/dist/cli.js +797 -287
  2. package/package.json +3 -1
package/dist/cli.js CHANGED
@@ -49,7 +49,7 @@ import {
49
49
  } from "./chunk-3RG5ZIWI.js";
50
50
 
51
51
  // src/cli.ts
52
- import React6 from "react";
52
+ import React7 from "react";
53
53
  import path19 from "path";
54
54
  import { Command } from "commander";
55
55
  import { render } from "ink";
@@ -470,7 +470,6 @@ async function loginWithBrowser(options) {
470
470
  // src/lib/llm/config.ts
471
471
  var OPENAI_AUTH_ONLY = process.env.OPENAI_AUTH_ONLY !== "false";
472
472
  var CODEX_RESPONSES_URL = process.env.CODEX_RESPONSES_URL ?? "https://chatgpt.com/backend-api/codex/responses";
473
- var OPENAI_VALIDATION_STALE_MS = 15 * 60 * 1e3;
474
473
 
475
474
  // src/lib/llm/openai-connection.ts
476
475
  var VALIDATION_INSTRUCTIONS = "You are validating whether this OpenAI Codex connection can execute a minimal request. Reply with the single word ok.";
@@ -829,25 +828,25 @@ function formatDateTime(value) {
829
828
  }
830
829
 
831
830
  // src/lib/cli/version.ts
832
- var PACKAGE_VERSION = "1.0.1";
831
+ var PACKAGE_VERSION = "1.1.0";
833
832
  function getPackageVersion() {
834
833
  return PACKAGE_VERSION;
835
834
  }
836
835
 
837
836
  // src/tui/app.tsx
838
837
  import path18 from "path";
839
- import {
838
+ import React6, {
840
839
  startTransition as startTransition2,
841
840
  useDeferredValue,
842
841
  useEffect as useEffect4,
843
- useMemo as useMemo3,
842
+ useMemo as useMemo4,
844
843
  useRef as useRef2,
845
- useState as useState6
844
+ useState as useState7
846
845
  } from "react";
847
- import { Box as Box5, Static, Text as Text5, useApp, useInput as useInput4 } from "ink";
846
+ import { Box as Box5, Static, Text as Text5, useApp, useInput as useInput5 } from "ink";
848
847
 
849
848
  // src/tui/text-input.tsx
850
- import { useState, useEffect, useRef } from "react";
849
+ import { useState, useEffect, useLayoutEffect, useRef } from "react";
851
850
  import { Box, Text, useInput, useStdout, measureElement } from "ink";
852
851
 
853
852
  // node_modules/chalk/source/vendor/ansi-styles/index.js
@@ -1995,30 +1994,37 @@ function TextInput({
1995
1994
  pasteMapRef.current.clear();
1996
1995
  }
1997
1996
  }, [originalValue]);
1998
- useEffect(() => {
1999
- const fallbackWidth = stdout.columns ?? 0;
2000
- if (!containerRef.current) {
2001
- if (fallbackWidth > 0) {
2002
- setInputWidth((current) => current === fallbackWidth ? current : fallbackWidth);
2003
- }
2004
- return;
2005
- }
2006
- const measuredWidth = measureElement(containerRef.current).width;
2007
- const nextWidth = measuredWidth > 0 ? measuredWidth : fallbackWidth;
2008
- if (nextWidth > 0) {
2009
- setInputWidth((current) => current === nextWidth ? current : nextWidth);
2010
- }
2011
- });
1997
+ useLayoutEffect(() => {
1998
+ setInputWidth((current) => {
1999
+ const fallbackWidth = typeof stdout.columns === "number" && stdout.columns > 0 ? stdout.columns : current;
2000
+ if (!containerRef.current) {
2001
+ return fallbackWidth > 0 && fallbackWidth !== current ? fallbackWidth : current;
2002
+ }
2003
+ const measuredWidth = measureElement(containerRef.current).width;
2004
+ const nextWidth = measuredWidth > 0 ? measuredWidth : fallbackWidth;
2005
+ if (nextWidth > 0 && nextWidth !== current) {
2006
+ return nextWidth;
2007
+ }
2008
+ return current;
2009
+ });
2010
+ }, [stdout.columns]);
2012
2011
  function pasteBadge(entry) {
2013
2012
  return applyThemeColor(source_default.dim, accentColor, `[Pasted text #${entry.id} +${entry.lineCount} lines]`);
2014
2013
  }
2015
- function getSlashCommandEnd() {
2016
- if (!originalValue.startsWith("/")) return 0;
2017
- const spaceIdx = originalValue.indexOf(" ");
2018
- return spaceIdx === -1 ? originalValue.length : spaceIdx;
2014
+ function getSlashCommandRange() {
2015
+ let slashIdx = originalValue.lastIndexOf("/");
2016
+ while (slashIdx > 0 && originalValue[slashIdx - 1] !== " ") {
2017
+ slashIdx = originalValue.lastIndexOf("/", slashIdx - 1);
2018
+ }
2019
+ if (slashIdx === -1) return { start: 0, end: 0 };
2020
+ const afterSlash = originalValue.indexOf(" ", slashIdx);
2021
+ const end = afterSlash === -1 ? originalValue.length : afterSlash;
2022
+ return { start: slashIdx, end };
2019
2023
  }
2020
2024
  function buildRendered() {
2021
- const cmdEnd = getSlashCommandEnd();
2025
+ const cmdRange = getSlashCommandRange();
2026
+ const hasCmd = cmdRange.end > cmdRange.start;
2027
+ const inCmd = (idx) => hasCmd && idx >= cmdRange.start && idx < cmdRange.end;
2022
2028
  if (showCursor && focus) {
2023
2029
  if (originalValue.length === 0) return source_default.inverse(" ");
2024
2030
  const pasteIds2 = [...pasteMapRef.current.keys()].sort((a, b) => a - b);
@@ -2037,12 +2043,12 @@ function TextInput({
2037
2043
  } else if (i2 === cursorOffset) {
2038
2044
  if (char === "\n") {
2039
2045
  result2 += source_default.inverse(" ") + "\n";
2040
- } else if (cmdEnd > 0 && i2 < cmdEnd) {
2046
+ } else if (inCmd(i2)) {
2041
2047
  result2 += applyThemeColor(source_default.inverse, accentColor, char);
2042
2048
  } else {
2043
2049
  result2 += source_default.inverse(char);
2044
2050
  }
2045
- } else if (cmdEnd > 0 && i2 < cmdEnd) {
2051
+ } else if (inCmd(i2)) {
2046
2052
  result2 += applyThemeColor(source_default, accentColor, char);
2047
2053
  } else {
2048
2054
  result2 += char;
@@ -2063,7 +2069,7 @@ function TextInput({
2063
2069
  const entry = pasteIdx < pasteIds.length ? pasteMapRef.current.get(pasteIds[pasteIdx]) : void 0;
2064
2070
  pasteIdx++;
2065
2071
  result += entry ? pasteBadge(entry) : "";
2066
- } else if (cmdEnd > 0 && i < cmdEnd) {
2072
+ } else if (inCmd(i)) {
2067
2073
  result += applyThemeColor(source_default, accentColor, char);
2068
2074
  } else {
2069
2075
  result += char;
@@ -2374,12 +2380,7 @@ var OPENAI_MODEL_MAP = {
2374
2380
  "gpt-5.4": "gpt-5.4",
2375
2381
  "gpt-5.4-mini": "gpt-5.4-mini",
2376
2382
  "openai/gpt-5.4": "gpt-5.4",
2377
- "openai/gpt-5.4-mini": "gpt-5.4-mini",
2378
- "google/gemini-3-flash-preview": "gpt-5.4",
2379
- "google/gemini-3.1-pro": "gpt-5.4",
2380
- "google/gemini-3.1-flash-lite-preview": "gpt-5.4-mini",
2381
- "google/gemini-2.5-flash-lite": "gpt-5.4-mini",
2382
- "google/gemini-2.5-flash": "gpt-5.4-mini"
2383
+ "openai/gpt-5.4-mini": "gpt-5.4-mini"
2383
2384
  };
2384
2385
  function resolveOpenAIModel(model) {
2385
2386
  return OPENAI_MODEL_MAP[model ?? "gpt-5.4"] ?? "gpt-5.4";
@@ -3650,14 +3651,24 @@ var TOOL_SCHEMAS = [
3650
3651
  type: "function",
3651
3652
  function: {
3652
3653
  name: "write_new_file",
3653
- description: "Create a new workspace file.",
3654
+ description: [
3655
+ "Create a new workspace file. The key determines where the file is placed:",
3656
+ "- `note:<descriptive-slug>` \u2192 notes/<slug>.md \u2014 analysis, summaries, briefs, memos",
3657
+ "- `paper:<descriptive-slug>` \u2192 papers/<slug>.tex \u2014 LaTeX drafts and manuscripts",
3658
+ "- `experiment:<descriptive-slug>` \u2192 experiments/<slug>.json \u2014 experiment configs and results",
3659
+ "- `source:<descriptive-slug>` \u2192 sources/<slug>.md \u2014 extracted source material",
3660
+ "- `path:<relative/path.ext>` \u2192 exact path \u2014 scripts, configs, data files, any custom location",
3661
+ "Use path: for code files (e.g. `path:scripts/analyze.py`, `path:data/results.csv`).",
3662
+ "Use descriptive slugs, not UUIDs: `note:transformer-scaling-laws` not `note:abc123`.",
3663
+ 'Use the folder param to organize within managed directories (e.g. folder: "lit-review").'
3664
+ ].join(" "),
3654
3665
  parameters: {
3655
3666
  type: "object",
3656
3667
  properties: {
3657
- key: { type: "string" },
3658
- label: { type: "string" },
3668
+ key: { type: "string", description: "File key with prefix determining placement: note:<slug>, paper:<slug>, experiment:<slug>, source:<slug>, or path:<relative/path>" },
3669
+ label: { type: "string", description: "Human-readable display name for the file" },
3659
3670
  content: { type: "string" },
3660
- folder: { type: "string" }
3671
+ folder: { type: "string", description: "Optional subfolder within the managed directory for organization" }
3661
3672
  },
3662
3673
  required: ["key", "label", "content"],
3663
3674
  additionalProperties: false
@@ -3808,32 +3819,56 @@ var TOOL_SCHEMAS = [
3808
3819
  type: "function",
3809
3820
  function: {
3810
3821
  name: "ask_user",
3811
- description: "Ask the user a question and wait for their response. Use this when you need clarification, a decision between options, or confirmation before proceeding. Provide clear options when possible. The user can also type a custom answer.",
3822
+ description: "Ask the user one or more questions and wait for their responses. Use when you need clarification, a decision, or confirmation before proceeding. You can batch up to 4 related questions in a single call \u2014 the user answers them all at once. Provide predefined options when possible. The user can arrow-key select or type a custom answer.",
3812
3823
  parameters: {
3813
3824
  type: "object",
3814
3825
  properties: {
3826
+ questions: {
3827
+ type: "array",
3828
+ minItems: 1,
3829
+ maxItems: 4,
3830
+ items: {
3831
+ type: "object",
3832
+ properties: {
3833
+ question: {
3834
+ type: "string",
3835
+ description: "Clear, specific question. State what you need to know and why."
3836
+ },
3837
+ options: {
3838
+ type: "array",
3839
+ items: {
3840
+ type: "object",
3841
+ properties: {
3842
+ label: { type: "string", description: "Short option label (1-5 words)" },
3843
+ description: { type: "string", description: "One-sentence explanation of what this choice means" }
3844
+ },
3845
+ required: ["label", "description"]
3846
+ },
3847
+ description: "Predefined options. Include 2-5 choices. The user can also type a custom answer."
3848
+ }
3849
+ },
3850
+ required: ["question"]
3851
+ },
3852
+ description: "One or more questions to ask. Batch related questions together (max 4)."
3853
+ },
3854
+ // Legacy single-question support (backward compat)
3815
3855
  question: {
3816
3856
  type: "string",
3817
- description: "The question to ask the user."
3857
+ description: "Single question (shorthand). Use 'questions' array for multiple."
3818
3858
  },
3819
3859
  options: {
3820
3860
  type: "array",
3821
3861
  items: {
3822
3862
  type: "object",
3823
3863
  properties: {
3824
- label: { type: "string", description: "Short option label (1-5 words)." },
3825
- description: { type: "string", description: "One-sentence description of this option." }
3864
+ label: { type: "string" },
3865
+ description: { type: "string" }
3826
3866
  },
3827
3867
  required: ["label", "description"]
3828
3868
  },
3829
- description: "Predefined options for the user to choose from."
3830
- },
3831
- allow_custom: {
3832
- type: "boolean",
3833
- description: "Whether the user can type a custom answer. Default: true."
3869
+ description: "Options for single question (shorthand)."
3834
3870
  }
3835
3871
  },
3836
- required: ["question"],
3837
3872
  additionalProperties: false
3838
3873
  }
3839
3874
  }
@@ -4547,36 +4582,52 @@ function clearPendingQuestion() {
4547
4582
  function resetPendingQuestions() {
4548
4583
  pendingQuestions = [];
4549
4584
  }
4585
+ function normalizeQuestions(args) {
4586
+ if (args.questions && args.questions.length > 0) {
4587
+ return args.questions.slice(0, 4);
4588
+ }
4589
+ if (args.question) {
4590
+ return [{ question: args.question, options: args.options }];
4591
+ }
4592
+ return [];
4593
+ }
4550
4594
  async function executeAskUser(args, signal) {
4551
- const questionId = crypto.randomUUID();
4552
- const question = {
4553
- id: questionId,
4554
- question: args.question,
4555
- options: (args.options ?? []).map((o) => ({
4556
- label: o.label,
4557
- description: o.description
4558
- })),
4559
- allowCustom: args.allow_custom ?? true
4560
- };
4561
- const answerPromise = new Promise((resolve, reject) => {
4562
- pendingQuestions.push({ question, resolve });
4563
- if (signal) {
4564
- const onAbort = () => {
4565
- pendingQuestions = pendingQuestions.filter((q) => q.question.id !== questionId);
4566
- reject(new Error("Question cancelled \u2014 user interrupted."));
4567
- };
4568
- if (signal.aborted) {
4569
- onAbort();
4570
- return;
4595
+ const items = normalizeQuestions(args);
4596
+ if (items.length === 0) {
4597
+ return "Error: no question provided.";
4598
+ }
4599
+ const answers = [];
4600
+ for (const item of items) {
4601
+ const questionId = crypto.randomUUID();
4602
+ const question = {
4603
+ id: questionId,
4604
+ question: item.question,
4605
+ options: (item.options ?? []).map((o) => ({
4606
+ label: o.label,
4607
+ description: o.description
4608
+ })),
4609
+ allowCustom: true
4610
+ };
4611
+ const answer = await new Promise((resolve, reject) => {
4612
+ pendingQuestions.push({ question, resolve });
4613
+ if (signal) {
4614
+ const onAbort = () => {
4615
+ pendingQuestions = pendingQuestions.filter((q) => q.question.id !== questionId);
4616
+ reject(new Error("Question cancelled \u2014 user interrupted."));
4617
+ };
4618
+ if (signal.aborted) {
4619
+ onAbort();
4620
+ return;
4621
+ }
4622
+ signal.addEventListener("abort", onAbort, { once: true });
4571
4623
  }
4572
- signal.addEventListener("abort", onAbort, { once: true });
4573
- }
4574
- });
4575
- const answer = await answerPromise;
4576
- if (answer.isCustom) {
4577
- return `User answered: "${answer.answer}"`;
4624
+ });
4625
+ const prefix = items.length > 1 ? `Q${answers.length + 1}: "${item.question}" \u2192 ` : "";
4626
+ answers.push(
4627
+ answer.isCustom ? `${prefix}User answered: "${answer.answer}"` : `${prefix}User selected: "${answer.answer}"`
4628
+ );
4578
4629
  }
4579
- return `User selected: "${answer.answer}"`;
4630
+ return answers.join("\n");
4580
4631
  }
4581
4632
 
4582
4633
  // src/lib/agent/tools/search-workspace.ts
@@ -4673,8 +4724,15 @@ function normalizeDbBackedKey(key) {
4673
4724
  }
4674
4725
  return `${prefix}:${crypto.randomUUID()}`;
4675
4726
  }
4727
+ var VALID_PREFIXES = ["note:", "paper:", "experiment:", "source:", "path:"];
4676
4728
  function executeWriteNewFile(args, ctx) {
4677
4729
  const normalizedKey = normalizeDbBackedKey(args.key);
4730
+ if (!VALID_PREFIXES.some((p) => normalizedKey.startsWith(p))) {
4731
+ return {
4732
+ result: `Error: Key "${normalizedKey}" is missing a required prefix. Use one of: note:<slug>, paper:<slug>, experiment:<slug>, source:<slug>, or path:<relative/path>. Example: note:${normalizedKey}`,
4733
+ update: null
4734
+ };
4735
+ }
4678
4736
  if (normalizedKey in ctx.workspaceFiles) {
4679
4737
  return {
4680
4738
  result: `Error: File "${normalizedKey}" already exists. Use update_existing_file to modify it.`,
@@ -4705,6 +4763,7 @@ function executeUpdateExistingFile(args, ctx) {
4705
4763
  };
4706
4764
  }
4707
4765
  const mode = args.mode ?? "rewrite";
4766
+ const oldContent = ctx.workspaceFiles[args.key];
4708
4767
  if (mode === "rewrite") {
4709
4768
  const content = args.content;
4710
4769
  if (content == null) {
@@ -4718,6 +4777,7 @@ function executeUpdateExistingFile(args, ctx) {
4718
4777
  type: "edit",
4719
4778
  key: args.key,
4720
4779
  content,
4780
+ oldContent,
4721
4781
  summary: args.summary
4722
4782
  };
4723
4783
  return {
@@ -4781,6 +4841,7 @@ ${preview}`,
4781
4841
  type: "edit",
4782
4842
  key: args.key,
4783
4843
  content: currentContent,
4844
+ oldContent,
4784
4845
  summary: autoSummary
4785
4846
  };
4786
4847
  return {
@@ -7291,6 +7352,17 @@ ${skill.prompt}`).join("\n\n");
7291
7352
  "- Redirect large outputs to files. Read selectively \u2014 don't dump entire datasets into responses.",
7292
7353
  "- Always wrap file paths in backticks: `notes/brief.md`, `experiments/analysis.py:42`.",
7293
7354
  "",
7355
+ "## Workspace Organization",
7356
+ "The workspace has managed directories. When creating files with `write_new_file`, use the correct key prefix:",
7357
+ "- `note:<slug>` \u2192 `notes/` \u2014 analysis write-ups, literature reviews, meeting notes, briefs",
7358
+ "- `paper:<slug>` \u2192 `papers/` \u2014 LaTeX manuscripts and drafts",
7359
+ "- `experiment:<slug>` \u2192 `experiments/` \u2014 experiment definitions, configs, results",
7360
+ "- `source:<slug>` \u2192 `sources/` \u2014 extracted text from papers, articles, datasets",
7361
+ "- `path:<relative/path>` \u2192 exact location \u2014 scripts, code, CSV data, configs, anything else",
7362
+ "Use descriptive slugs that read naturally: `note:scaling-law-comparison`, `experiment:ablation-dropout-rates`, `path:scripts/parse-arxiv.py`.",
7363
+ "Use the `folder` param to group related files: e.g. key `note:gpt4-findings` with folder `lit-review` creates `notes/lit-review/gpt4-findings.md`.",
7364
+ "Never use bare keys without a prefix \u2014 they end up in `artifacts/` which is not user-facing.",
7365
+ "",
7294
7366
  `## Workspace
7295
7367
  Root: ${process.cwd()}`,
7296
7368
  skillText
@@ -7592,20 +7664,22 @@ function classifyUpdateRisk(update) {
7592
7664
  import fs14 from "fs/promises";
7593
7665
  import path14 from "path";
7594
7666
  function resolveRelativePath(update) {
7667
+ const folder = update.folder;
7668
+ const sub = (dir, name, ext) => folder ? `${dir}/${folder}/${name}${ext}` : `${dir}/${name}${ext}`;
7595
7669
  if (update.key.startsWith("path:")) {
7596
7670
  return update.key.slice(5);
7597
7671
  }
7598
7672
  if (update.key.startsWith("note:")) {
7599
- return `notes/${update.key.slice(5)}.md`;
7673
+ return sub("notes", update.key.slice(5), ".md");
7600
7674
  }
7601
7675
  if (update.key.startsWith("paper:")) {
7602
- return `papers/${update.key.slice(6)}.tex`;
7676
+ return sub("papers", update.key.slice(6), ".tex");
7603
7677
  }
7604
7678
  if (update.key.startsWith("experiment:")) {
7605
- return `experiments/${update.key.slice(11)}.json`;
7679
+ return sub("experiments", update.key.slice(11), ".json");
7606
7680
  }
7607
7681
  if (update.key.startsWith("source:")) {
7608
- return `sources/${update.key.slice(7)}.md`;
7682
+ return sub("sources", update.key.slice(7), ".md");
7609
7683
  }
7610
7684
  return `artifacts/${update.key}.md`;
7611
7685
  }
@@ -8014,6 +8088,14 @@ function getUnifiedSuggestions(partial, allSkills) {
8014
8088
  }));
8015
8089
  return [...cmdHits, ...skillHits];
8016
8090
  }
8091
+ function extractSlashTrigger(text) {
8092
+ const lastSlash = text.lastIndexOf("/");
8093
+ if (lastSlash === -1) return null;
8094
+ if (lastSlash > 0 && text[lastSlash - 1] !== " ") return null;
8095
+ const after = text.slice(lastSlash + 1);
8096
+ if (after.includes(" ")) return null;
8097
+ return { partial: after.toLowerCase(), start: lastSlash };
8098
+ }
8017
8099
  function extractAtMention(text) {
8018
8100
  const lastAt = text.lastIndexOf("@");
8019
8101
  if (lastAt === -1) return null;
@@ -8038,7 +8120,9 @@ function truncate3(value, max = 96) {
8038
8120
  }
8039
8121
 
8040
8122
  // src/tui/components.tsx
8041
- import { Box as Box4, Text as Text4 } from "ink";
8123
+ import { memo, useMemo as useMemo3, useState as useState4 } from "react";
8124
+ import { Box as Box4, Text as Text4, useInput as useInput4 } from "ink";
8125
+ import { structuredPatch } from "diff";
8042
8126
 
8043
8127
  // src/tui/layout.ts
8044
8128
  var DEFAULT_TERMINAL_WIDTH = 80;
@@ -8046,11 +8130,19 @@ var MIN_TERMINAL_WIDTH = 20;
8046
8130
  function getTerminalWidth(columns) {
8047
8131
  return Math.max(MIN_TERMINAL_WIDTH, columns ?? process.stdout.columns ?? DEFAULT_TERMINAL_WIDTH);
8048
8132
  }
8049
- function getObservedTerminalWidth(...columns) {
8133
+ function normalizeObservedTerminalWidth(fallbackWidth, ...columns) {
8050
8134
  const observed = columns.filter((value) => typeof value === "number" && Number.isFinite(value) && value > 0);
8051
- if (observed.length === 0) return DEFAULT_TERMINAL_WIDTH;
8135
+ if (observed.length === 0) {
8136
+ return Math.max(MIN_TERMINAL_WIDTH, fallbackWidth);
8137
+ }
8052
8138
  return Math.max(MIN_TERMINAL_WIDTH, Math.min(...observed));
8053
8139
  }
8140
+ function getObservedTerminalWidth(...columns) {
8141
+ return normalizeObservedTerminalWidth(DEFAULT_TERMINAL_WIDTH, ...columns);
8142
+ }
8143
+ function getStableObservedTerminalWidth(currentWidth, ...columns) {
8144
+ return normalizeObservedTerminalWidth(currentWidth, ...columns);
8145
+ }
8054
8146
  function insetWidth(width, inset) {
8055
8147
  return Math.max(1, width - inset);
8056
8148
  }
@@ -8166,7 +8258,7 @@ function renderInline(text, codeColor = source_default.cyan, linkColor = source_
8166
8258
  }
8167
8259
 
8168
8260
  // src/tui/components.tsx
8169
- import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
8261
+ import { Fragment, jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
8170
8262
  var GUTTER = {
8171
8263
  user: "\u203A",
8172
8264
  agent: "\u25AA",
@@ -8190,7 +8282,12 @@ function borderedContentWidth(width) {
8190
8282
  function wrapText(value, width) {
8191
8283
  return wrapAnsi(value, Math.max(1, width), { trim: false, hard: true });
8192
8284
  }
8193
- function UserMessage({ text, width }) {
8285
+ function Divider({ width, color }) {
8286
+ const theme = useTheme();
8287
+ const w = insetWidth(resolveWidth(width), 4);
8288
+ return /* @__PURE__ */ jsx4(Text4, { color: color ?? theme.muted, dimColor: true, children: "\u2500".repeat(Math.max(1, w)) });
8289
+ }
8290
+ var UserMessage = memo(function UserMessage2({ text, turnNumber, width }) {
8194
8291
  const theme = useTheme();
8195
8292
  const contentWidth = resolveWidth(width);
8196
8293
  const bodyWidth = indentedWidth(contentWidth);
@@ -8201,12 +8298,18 @@ function UserMessage({ text, width }) {
8201
8298
  GUTTER.user,
8202
8299
  " "
8203
8300
  ] }),
8204
- /* @__PURE__ */ jsx4(Text4, { bold: true, color: theme.accent, children: "you" })
8301
+ /* @__PURE__ */ jsx4(Text4, { bold: true, color: theme.accent, children: "you" }),
8302
+ typeof turnNumber === "number" && /* @__PURE__ */ jsxs3(Text4, { color: theme.muted, dimColor: true, children: [
8303
+ " ",
8304
+ GUTTER.system,
8305
+ " #",
8306
+ turnNumber
8307
+ ] })
8205
8308
  ] }),
8206
8309
  /* @__PURE__ */ jsx4(Box4, { marginLeft: 2, width: bodyWidth, children: /* @__PURE__ */ jsx4(Text4, { color: theme.text, wrap: "wrap", children: wrappedText }) })
8207
8310
  ] });
8208
- }
8209
- function AgentMessage({ text, width }) {
8311
+ });
8312
+ var AgentMessage = memo(function AgentMessage2({ text, width }) {
8210
8313
  const contentWidth = resolveWidth(width);
8211
8314
  const bodyWidth = indentedWidth(contentWidth);
8212
8315
  const theme = useTheme();
@@ -8222,6 +8325,20 @@ function AgentMessage({ text, width }) {
8222
8325
  ] }),
8223
8326
  /* @__PURE__ */ jsx4(Box4, { marginLeft: 2, width: bodyWidth, children: /* @__PURE__ */ jsx4(Text4, { wrap: "wrap", children: wrappedText }) })
8224
8327
  ] });
8328
+ });
8329
+ function ThinkingIndicator({ frame, width }) {
8330
+ const theme = useTheme();
8331
+ const contentWidth = resolveWidth(width);
8332
+ return /* @__PURE__ */ jsxs3(Box4, { marginBottom: 1, width: contentWidth, children: [
8333
+ /* @__PURE__ */ jsxs3(Text4, { color: theme.secondary, bold: true, children: [
8334
+ GUTTER.agent,
8335
+ " "
8336
+ ] }),
8337
+ /* @__PURE__ */ jsxs3(Text4, { color: theme.muted, dimColor: true, children: [
8338
+ frame,
8339
+ " thinking..."
8340
+ ] })
8341
+ ] });
8225
8342
  }
8226
8343
  function TaskPanel({
8227
8344
  tasks: tasks2,
@@ -8254,7 +8371,7 @@ function TaskPanel({
8254
8371
  ] })
8255
8372
  ] });
8256
8373
  }
8257
- function ToolActivitySummary({
8374
+ var ToolActivitySummary = memo(function ToolActivitySummary2({
8258
8375
  summary,
8259
8376
  tools,
8260
8377
  expanded = false,
@@ -8262,43 +8379,50 @@ function ToolActivitySummary({
8262
8379
  }) {
8263
8380
  const theme = useTheme();
8264
8381
  const contentWidth = indentedWidth(resolveWidth(width));
8382
+ const innerWidth = indentedWidth(contentWidth, 4);
8265
8383
  if (expanded) {
8266
- return /* @__PURE__ */ jsxs3(Box4, { flexDirection: "column", marginLeft: 2, marginBottom: 0, width: contentWidth, children: [
8267
- /* @__PURE__ */ jsxs3(Text4, { color: theme.muted, dimColor: true, wrap: "wrap", children: [
8268
- GUTTER.tool,
8269
- " ",
8270
- summary
8271
- ] }),
8272
- tools.map((t, i) => {
8273
- const dur = t.durationMs ? ` (${(t.durationMs / 1e3).toFixed(1)}s)` : "";
8274
- const prefix = i === tools.length - 1 ? "\u2514" : "\u251C";
8275
- return /* @__PURE__ */ jsxs3(Text4, { color: theme.muted, dimColor: true, wrap: "wrap", children: [
8276
- " ",
8277
- prefix,
8384
+ return /* @__PURE__ */ jsxs3(Box4, { flexDirection: "row", marginLeft: 2, marginBottom: 0, width: contentWidth, children: [
8385
+ /* @__PURE__ */ jsx4(Text4, { color: theme.muted, dimColor: true, children: "\u2502 " }),
8386
+ /* @__PURE__ */ jsxs3(Box4, { flexDirection: "column", width: innerWidth, children: [
8387
+ /* @__PURE__ */ jsxs3(Text4, { color: theme.muted, dimColor: true, wrap: "wrap", children: [
8388
+ GUTTER.tool,
8278
8389
  " ",
8279
- GUTTER.success,
8280
- " ",
8281
- t.description,
8282
- dur
8283
- ] }, i);
8284
- })
8390
+ summary
8391
+ ] }),
8392
+ tools.map((t, i) => {
8393
+ const dur = t.durationMs ? ` (${(t.durationMs / 1e3).toFixed(1)}s)` : "";
8394
+ const prefix = i === tools.length - 1 ? "\u2514" : "\u251C";
8395
+ return /* @__PURE__ */ jsxs3(Text4, { color: theme.muted, dimColor: true, wrap: "wrap", children: [
8396
+ " ",
8397
+ prefix,
8398
+ " ",
8399
+ GUTTER.success,
8400
+ " ",
8401
+ t.description,
8402
+ dur
8403
+ ] }, i);
8404
+ })
8405
+ ] })
8285
8406
  ] });
8286
8407
  }
8287
8408
  const lastTarget = tools.length > 0 ? tools[tools.length - 1].description : "";
8288
8409
  const hint = tools.length > 1 ? " (ctrl+o to expand)" : "";
8289
- return /* @__PURE__ */ jsxs3(Box4, { flexDirection: "column", marginLeft: 2, marginBottom: 0, width: contentWidth, children: [
8290
- /* @__PURE__ */ jsxs3(Text4, { color: theme.muted, dimColor: true, wrap: "wrap", children: [
8291
- GUTTER.tool,
8292
- " ",
8293
- summary,
8294
- hint
8295
- ] }),
8296
- lastTarget && tools.length > 1 && /* @__PURE__ */ jsxs3(Text4, { color: theme.muted, dimColor: true, wrap: "wrap", children: [
8297
- " \u2514 ",
8298
- lastTarget
8410
+ return /* @__PURE__ */ jsxs3(Box4, { flexDirection: "row", marginLeft: 2, marginBottom: 0, width: contentWidth, children: [
8411
+ /* @__PURE__ */ jsx4(Text4, { color: theme.muted, dimColor: true, children: "\u2502 " }),
8412
+ /* @__PURE__ */ jsxs3(Box4, { flexDirection: "column", width: innerWidth, children: [
8413
+ /* @__PURE__ */ jsxs3(Text4, { color: theme.muted, dimColor: true, wrap: "wrap", children: [
8414
+ GUTTER.tool,
8415
+ " ",
8416
+ summary,
8417
+ hint
8418
+ ] }),
8419
+ lastTarget && tools.length > 1 && /* @__PURE__ */ jsxs3(Text4, { color: theme.muted, dimColor: true, wrap: "wrap", children: [
8420
+ " \u2514 ",
8421
+ lastTarget
8422
+ ] })
8299
8423
  ] })
8300
8424
  ] });
8301
- }
8425
+ });
8302
8426
  function SubAgentIndicator({
8303
8427
  agentType,
8304
8428
  goal,
@@ -8347,23 +8471,30 @@ function SubAgentIndicator({
8347
8471
  }
8348
8472
  );
8349
8473
  }
8350
- function SystemMessage({ text, width }) {
8474
+ var SystemMessage = memo(function SystemMessage2({ text, width }) {
8351
8475
  const theme = useTheme();
8352
8476
  const contentWidth = resolveWidth(width);
8353
8477
  const indentedContentWidth = indentedWidth(contentWidth);
8354
8478
  const wrappedIndentedText = wrapText(text, indentedContentWidth);
8355
8479
  const wrappedText = wrapText(text, contentWidth);
8356
- if (text.trimStart().startsWith("\u2713") || text.trimStart().startsWith("\u2717")) {
8480
+ const trimmed = text.trimStart();
8481
+ if (trimmed.startsWith("\u2713") || trimmed.startsWith("\u2717")) {
8357
8482
  return /* @__PURE__ */ jsx4(Box4, { marginLeft: 2, width: indentedContentWidth, children: /* @__PURE__ */ jsx4(Text4, { color: theme.muted, dimColor: true, wrap: "wrap", children: wrappedIndentedText }) });
8358
8483
  }
8359
- if (text.includes("compacted") || text.includes("Context")) {
8484
+ if (trimmed.startsWith("Error:") || trimmed.startsWith("Failed:")) {
8485
+ return /* @__PURE__ */ jsx4(Box4, { marginLeft: 2, width: indentedContentWidth, children: /* @__PURE__ */ jsx4(Text4, { color: theme.error, wrap: "wrap", children: wrapText(`${GUTTER.error} ${text.trim()}`, indentedContentWidth) }) });
8486
+ }
8487
+ if (trimmed.includes("\u25CA remembered:") || trimmed.includes("\u25CA ontology") || trimmed.includes("\u25CA learned:")) {
8488
+ return /* @__PURE__ */ jsx4(Box4, { marginLeft: 2, width: indentedContentWidth, children: /* @__PURE__ */ jsx4(Text4, { color: theme.accent, dimColor: true, wrap: "wrap", children: wrapText(text.trim(), indentedContentWidth) }) });
8489
+ }
8490
+ if (text.includes("compacted") || text.includes("Context compacted")) {
8360
8491
  return /* @__PURE__ */ jsx4(Box4, { marginLeft: 2, width: indentedContentWidth, children: /* @__PURE__ */ jsx4(Text4, { color: theme.warning, dimColor: true, wrap: "wrap", children: wrapText(`${GUTTER.system} ${text.trim()}`, indentedContentWidth) }) });
8361
8492
  }
8362
- if (text.trimStart().startsWith(">")) {
8493
+ if (trimmed.startsWith(">")) {
8363
8494
  return /* @__PURE__ */ jsx4(Box4, { width: contentWidth, children: /* @__PURE__ */ jsx4(Text4, { color: theme.muted, dimColor: true, wrap: "wrap", children: wrappedText }) });
8364
8495
  }
8365
8496
  return /* @__PURE__ */ jsx4(Box4, { width: contentWidth, children: /* @__PURE__ */ jsx4(Text4, { color: theme.muted, wrap: "wrap", children: wrapText(`${GUTTER.system} ${text.trim()}`, contentWidth) }) });
8366
- }
8497
+ });
8367
8498
  function PromptPrefix({
8368
8499
  busy,
8369
8500
  frame,
@@ -8382,25 +8513,131 @@ function PromptPrefix({
8382
8513
  " "
8383
8514
  ] });
8384
8515
  }
8516
+ var MAX_DIFF_LINES = 16;
8517
+ function computeDiffLines(oldContent, newContent, fileName) {
8518
+ if (oldContent == null) {
8519
+ const lines = newContent.split("\n");
8520
+ return lines.slice(0, MAX_DIFF_LINES + 4).map((l) => ({ type: "add", text: l }));
8521
+ }
8522
+ const patch = structuredPatch(fileName, fileName, oldContent, newContent, "", "", { context: 2 });
8523
+ const result = [];
8524
+ for (const hunk of patch.hunks) {
8525
+ if (result.length > 0) {
8526
+ result.push({ type: "ctx", text: "\xB7\xB7\xB7" });
8527
+ }
8528
+ for (const line of hunk.lines) {
8529
+ if (line.startsWith("+")) {
8530
+ result.push({ type: "add", text: line.slice(1) });
8531
+ } else if (line.startsWith("-")) {
8532
+ result.push({ type: "del", text: line.slice(1) });
8533
+ } else {
8534
+ result.push({ type: "ctx", text: line.slice(1) });
8535
+ }
8536
+ }
8537
+ }
8538
+ return result;
8539
+ }
8385
8540
  function PendingUpdateCard({
8386
8541
  count,
8387
8542
  summary,
8543
+ fileName,
8544
+ updateType,
8545
+ oldContent,
8546
+ newContent,
8547
+ active,
8548
+ onAccept,
8549
+ onReject,
8550
+ onFeedback,
8388
8551
  width
8389
8552
  }) {
8390
8553
  const theme = useTheme();
8391
8554
  const contentWidth = resolveWidth(width);
8392
- const bodyWidth = indentedWidth(borderedContentWidth(contentWidth));
8555
+ const innerWidth = borderedContentWidth(contentWidth);
8556
+ const bodyWidth = indentedWidth(innerWidth);
8557
+ const diffLines = useMemo3(
8558
+ () => computeDiffLines(oldContent, newContent, fileName),
8559
+ [oldContent, newContent, fileName]
8560
+ );
8561
+ const truncated = diffLines.length > MAX_DIFF_LINES;
8562
+ const visibleLines = truncated ? diffLines.slice(0, MAX_DIFF_LINES) : diffLines;
8563
+ const additions = diffLines.filter((l) => l.type === "add").length;
8564
+ const deletions = diffLines.filter((l) => l.type === "del").length;
8565
+ const options = [
8566
+ { label: "Accept", description: "Apply this update" },
8567
+ { label: "Reject", description: "Discard this update" }
8568
+ ];
8569
+ const totalItems = options.length + 1;
8570
+ const feedbackIndex = options.length;
8571
+ const [selectedIndex, setSelectedIndex] = useState4(0);
8572
+ const [mode, setMode] = useState4("selecting");
8573
+ const [feedbackText, setFeedbackText] = useState4("");
8574
+ useInput4((input2, key) => {
8575
+ if (!active || mode !== "selecting") return;
8576
+ if (key.upArrow) {
8577
+ setSelectedIndex((i) => Math.max(0, i - 1));
8578
+ return;
8579
+ }
8580
+ if (key.downArrow) {
8581
+ setSelectedIndex((i) => Math.min(totalItems - 1, i + 1));
8582
+ return;
8583
+ }
8584
+ if (key.return) {
8585
+ if (selectedIndex === 0) {
8586
+ onAccept();
8587
+ return;
8588
+ }
8589
+ if (selectedIndex === 1) {
8590
+ onReject();
8591
+ return;
8592
+ }
8593
+ if (selectedIndex === feedbackIndex) {
8594
+ setMode("typing");
8595
+ setFeedbackText("");
8596
+ }
8597
+ return;
8598
+ }
8599
+ }, { isActive: active && mode === "selecting" });
8600
+ useInput4((input2, key) => {
8601
+ if (!active || mode !== "typing") return;
8602
+ if (key.escape) {
8603
+ setMode("selecting");
8604
+ setFeedbackText("");
8605
+ return;
8606
+ }
8607
+ if (key.return) {
8608
+ if (feedbackText.trim()) {
8609
+ onFeedback(feedbackText.trim());
8610
+ setFeedbackText("");
8611
+ setMode("selecting");
8612
+ }
8613
+ return;
8614
+ }
8615
+ if (key.backspace || key.delete) {
8616
+ setFeedbackText((t) => t.slice(0, -1));
8617
+ return;
8618
+ }
8619
+ if (key.ctrl && input2 === "u") {
8620
+ setFeedbackText("");
8621
+ return;
8622
+ }
8623
+ if (!key.ctrl && !key.meta && !key.tab && input2.length === 1 && input2 >= " ") {
8624
+ setFeedbackText((t) => t + input2);
8625
+ }
8626
+ }, { isActive: active && mode === "typing" });
8627
+ const isFeedbackSelected = selectedIndex === feedbackIndex;
8628
+ const lineNumWidth = Math.max(3, String(diffLines.length).length + 1);
8629
+ const diffContentWidth = Math.max(1, bodyWidth - lineNumWidth - 3);
8393
8630
  return /* @__PURE__ */ jsxs3(
8394
8631
  Box4,
8395
8632
  {
8396
8633
  borderStyle: "single",
8397
- borderColor: theme.pending,
8634
+ borderColor: theme.warning,
8398
8635
  paddingX: 1,
8399
- marginBottom: 1,
8636
+ marginBottom: 0,
8400
8637
  flexDirection: "column",
8401
8638
  width: contentWidth,
8402
8639
  children: [
8403
- /* @__PURE__ */ jsxs3(Box4, { width: borderedContentWidth(contentWidth), children: [
8640
+ /* @__PURE__ */ jsxs3(Box4, { width: innerWidth, children: [
8404
8641
  /* @__PURE__ */ jsxs3(Text4, { color: theme.pending, bold: true, children: [
8405
8642
  GUTTER.pending,
8406
8643
  " "
@@ -8412,13 +8649,70 @@ function PendingUpdateCard({
8412
8649
  " awaiting review"
8413
8650
  ] })
8414
8651
  ] }),
8415
- /* @__PURE__ */ jsx4(Box4, { marginLeft: 2, width: bodyWidth, children: /* @__PURE__ */ jsx4(Text4, { color: theme.muted, wrap: "wrap", children: summary }) }),
8416
- /* @__PURE__ */ jsx4(Box4, { marginLeft: 2, marginTop: 0, width: bodyWidth, children: /* @__PURE__ */ jsxs3(Text4, { color: theme.muted, dimColor: true, wrap: "wrap", children: [
8417
- /* @__PURE__ */ jsx4(Text4, { bold: true, color: theme.secondary, children: "a" }),
8418
- " accept ",
8419
- /* @__PURE__ */ jsx4(Text4, { bold: true, color: theme.error, children: "r" }),
8420
- " reject"
8421
- ] }) })
8652
+ /* @__PURE__ */ jsxs3(Box4, { marginLeft: 2, marginTop: 0, width: bodyWidth, children: [
8653
+ /* @__PURE__ */ jsx4(Text4, { color: theme.muted, wrap: "wrap", children: summary }),
8654
+ /* @__PURE__ */ jsx4(Text4, { color: theme.muted, dimColor: true, children: " " }),
8655
+ additions > 0 && /* @__PURE__ */ jsxs3(Text4, { color: theme.secondary, children: [
8656
+ "+",
8657
+ additions
8658
+ ] }),
8659
+ additions > 0 && deletions > 0 && /* @__PURE__ */ jsx4(Text4, { color: theme.muted, dimColor: true, children: " " }),
8660
+ deletions > 0 && /* @__PURE__ */ jsxs3(Text4, { color: theme.error, children: [
8661
+ "-",
8662
+ deletions
8663
+ ] })
8664
+ ] }),
8665
+ /* @__PURE__ */ jsxs3(Box4, { flexDirection: "column", marginLeft: 2, marginTop: 1, width: bodyWidth, children: [
8666
+ visibleLines.map((line, i) => {
8667
+ const prefix = line.type === "add" ? "+" : line.type === "del" ? "-" : " ";
8668
+ const color = line.type === "add" ? theme.secondary : line.type === "del" ? theme.error : theme.muted;
8669
+ const dimmed = line.type === "ctx";
8670
+ const displayText = truncateToWidth(line.text, diffContentWidth);
8671
+ return /* @__PURE__ */ jsxs3(Text4, { color, dimColor: dimmed, wrap: "truncate-end", children: [
8672
+ prefix,
8673
+ " ",
8674
+ displayText
8675
+ ] }, i);
8676
+ }),
8677
+ truncated && /* @__PURE__ */ jsxs3(Text4, { color: theme.muted, dimColor: true, children: [
8678
+ " \xB7\xB7\xB7 ",
8679
+ diffLines.length - MAX_DIFF_LINES,
8680
+ " more lines"
8681
+ ] })
8682
+ ] }),
8683
+ /* @__PURE__ */ jsxs3(Box4, { flexDirection: "column", marginLeft: 2, marginTop: 1, width: bodyWidth, children: [
8684
+ options.map((opt, idx) => {
8685
+ const isSelected = active && mode === "selecting" && idx === selectedIndex;
8686
+ const optColor = idx === 0 ? theme.secondary : theme.error;
8687
+ return /* @__PURE__ */ jsx4(Box4, { width: bodyWidth, children: isSelected ? /* @__PURE__ */ jsxs3(Fragment, { children: [
8688
+ /* @__PURE__ */ jsx4(Text4, { inverse: true, bold: true, color: theme.accent, children: " \u203A " }),
8689
+ /* @__PURE__ */ jsx4(Text4, { inverse: true, bold: true, children: " " + opt.label + " " }),
8690
+ /* @__PURE__ */ jsxs3(Text4, { color: theme.muted, dimColor: true, children: [
8691
+ " \u2014 ",
8692
+ opt.description
8693
+ ] })
8694
+ ] }) : /* @__PURE__ */ jsxs3(Fragment, { children: [
8695
+ /* @__PURE__ */ jsx4(Text4, { color: theme.muted, children: " " }),
8696
+ /* @__PURE__ */ jsx4(Text4, { color: optColor, children: opt.label }),
8697
+ /* @__PURE__ */ jsxs3(Text4, { color: theme.muted, dimColor: true, children: [
8698
+ " \u2014 ",
8699
+ opt.description
8700
+ ] })
8701
+ ] }) }, opt.label);
8702
+ }),
8703
+ mode === "typing" ? /* @__PURE__ */ jsxs3(Box4, { width: bodyWidth, children: [
8704
+ /* @__PURE__ */ jsx4(Text4, { color: theme.accent, bold: true, children: " \u203A " }),
8705
+ /* @__PURE__ */ jsx4(Text4, { color: theme.text, children: feedbackText }),
8706
+ /* @__PURE__ */ jsx4(Text4, { color: theme.accent, children: "\u2588" })
8707
+ ] }) : /* @__PURE__ */ jsx4(Box4, { width: bodyWidth, children: active && isFeedbackSelected ? /* @__PURE__ */ jsxs3(Fragment, { children: [
8708
+ /* @__PURE__ */ jsx4(Text4, { inverse: true, bold: true, color: theme.accent, children: " \u203A " }),
8709
+ /* @__PURE__ */ jsx4(Text4, { inverse: true, bold: true, color: theme.muted, children: " Give feedback... " })
8710
+ ] }) : /* @__PURE__ */ jsxs3(Fragment, { children: [
8711
+ /* @__PURE__ */ jsx4(Text4, { color: theme.muted, children: " " }),
8712
+ /* @__PURE__ */ jsx4(Text4, { color: theme.muted, dimColor: true, children: "Give feedback..." })
8713
+ ] }) })
8714
+ ] }),
8715
+ /* @__PURE__ */ jsx4(Box4, { marginLeft: 2, marginTop: 0, width: bodyWidth, children: /* @__PURE__ */ jsx4(Text4, { color: theme.muted, dimColor: true, wrap: "wrap", children: mode === "typing" ? "Type your feedback \xB7 Enter submit \xB7 Esc back" : active ? "\u2191/\u2193 select \xB7 Enter confirm" : "Waiting..." }) })
8422
8716
  ]
8423
8717
  }
8424
8718
  );
@@ -8426,12 +8720,67 @@ function PendingUpdateCard({
8426
8720
  function QuestionCard({
8427
8721
  question,
8428
8722
  options,
8723
+ active,
8724
+ onSelect,
8429
8725
  width
8430
8726
  }) {
8431
8727
  const theme = useTheme();
8432
8728
  const contentWidth = resolveWidth(width);
8433
8729
  const innerWidth = borderedContentWidth(contentWidth);
8434
8730
  const bodyWidth = indentedWidth(innerWidth);
8731
+ const hasOptions = options.length > 0;
8732
+ const totalItems = hasOptions ? options.length + 1 : 0;
8733
+ const customIndex = options.length;
8734
+ const [selectedIndex, setSelectedIndex] = useState4(0);
8735
+ const [mode, setMode] = useState4(hasOptions ? "selecting" : "typing");
8736
+ const [customText, setCustomText] = useState4("");
8737
+ useInput4((input2, key) => {
8738
+ if (!active || mode !== "selecting" || !hasOptions) return;
8739
+ if (key.upArrow) {
8740
+ setSelectedIndex((i) => Math.max(0, i - 1));
8741
+ return;
8742
+ }
8743
+ if (key.downArrow) {
8744
+ setSelectedIndex((i) => Math.min(totalItems - 1, i + 1));
8745
+ return;
8746
+ }
8747
+ if (key.return) {
8748
+ if (selectedIndex < options.length) {
8749
+ const picked = options[selectedIndex];
8750
+ if (picked) onSelect(picked.label, false);
8751
+ } else {
8752
+ setMode("typing");
8753
+ setCustomText("");
8754
+ }
8755
+ return;
8756
+ }
8757
+ }, { isActive: active && mode === "selecting" });
8758
+ useInput4((input2, key) => {
8759
+ if (!active || mode !== "typing") return;
8760
+ if (key.escape && hasOptions) {
8761
+ setMode("selecting");
8762
+ setCustomText("");
8763
+ return;
8764
+ }
8765
+ if (key.return) {
8766
+ if (customText.trim()) {
8767
+ onSelect(customText.trim(), true);
8768
+ }
8769
+ return;
8770
+ }
8771
+ if (key.backspace || key.delete) {
8772
+ setCustomText((t) => t.slice(0, -1));
8773
+ return;
8774
+ }
8775
+ if (key.ctrl && input2 === "u") {
8776
+ setCustomText("");
8777
+ return;
8778
+ }
8779
+ if (!key.ctrl && !key.meta && !key.tab && input2.length === 1 && input2 >= " ") {
8780
+ setCustomText((t) => t + input2);
8781
+ }
8782
+ }, { isActive: active && mode === "typing" });
8783
+ const isCustomSelected = selectedIndex === customIndex;
8435
8784
  return /* @__PURE__ */ jsxs3(
8436
8785
  Box4,
8437
8786
  {
@@ -8450,18 +8799,38 @@ function QuestionCard({
8450
8799
  /* @__PURE__ */ jsx4(Text4, { bold: true, color: theme.warning, children: "Agent needs your input" })
8451
8800
  ] }),
8452
8801
  /* @__PURE__ */ jsx4(Box4, { marginLeft: 2, marginTop: 0, width: bodyWidth, children: /* @__PURE__ */ jsx4(Text4, { color: theme.text, wrap: "wrap", children: question }) }),
8453
- options.length > 0 && /* @__PURE__ */ jsx4(Box4, { flexDirection: "column", marginLeft: 2, marginTop: 1, width: bodyWidth, children: options.map((opt, idx) => /* @__PURE__ */ jsxs3(Box4, { width: bodyWidth, children: [
8454
- /* @__PURE__ */ jsx4(Text4, { color: theme.accent, bold: true, children: idx + 1 }),
8455
- /* @__PURE__ */ jsxs3(Text4, { color: theme.text, wrap: "wrap", children: [
8456
- " ",
8457
- opt.label
8458
- ] }),
8459
- /* @__PURE__ */ jsxs3(Text4, { color: theme.muted, dimColor: true, wrap: "wrap", children: [
8460
- " \u2014 ",
8461
- opt.description
8462
- ] })
8463
- ] }, opt.label)) }),
8464
- /* @__PURE__ */ jsx4(Box4, { marginLeft: 2, marginTop: 0, width: bodyWidth, children: /* @__PURE__ */ jsx4(Text4, { color: theme.muted, dimColor: true, wrap: "wrap", children: options.length > 0 ? "Type number or custom answer" : "Type your answer" }) })
8802
+ /* @__PURE__ */ jsxs3(Box4, { flexDirection: "column", marginLeft: 2, marginTop: 1, width: bodyWidth, children: [
8803
+ options.map((opt, idx) => {
8804
+ const isSelected = active && mode === "selecting" && idx === selectedIndex;
8805
+ return /* @__PURE__ */ jsx4(Box4, { width: bodyWidth, children: isSelected ? /* @__PURE__ */ jsxs3(Fragment, { children: [
8806
+ /* @__PURE__ */ jsx4(Text4, { inverse: true, bold: true, color: theme.accent, children: " \u203A " }),
8807
+ /* @__PURE__ */ jsx4(Text4, { inverse: true, bold: true, children: " " + opt.label + " " }),
8808
+ /* @__PURE__ */ jsxs3(Text4, { color: theme.muted, dimColor: true, children: [
8809
+ " \u2014 ",
8810
+ opt.description
8811
+ ] })
8812
+ ] }) : /* @__PURE__ */ jsxs3(Fragment, { children: [
8813
+ /* @__PURE__ */ jsx4(Text4, { color: theme.muted, children: " " }),
8814
+ /* @__PURE__ */ jsx4(Text4, { color: theme.text, children: opt.label }),
8815
+ /* @__PURE__ */ jsxs3(Text4, { color: theme.muted, dimColor: true, children: [
8816
+ " \u2014 ",
8817
+ opt.description
8818
+ ] })
8819
+ ] }) }, opt.label);
8820
+ }),
8821
+ mode === "typing" ? /* @__PURE__ */ jsxs3(Box4, { width: bodyWidth, children: [
8822
+ /* @__PURE__ */ jsx4(Text4, { color: theme.accent, bold: true, children: " \u203A " }),
8823
+ /* @__PURE__ */ jsx4(Text4, { color: theme.text, children: customText }),
8824
+ /* @__PURE__ */ jsx4(Text4, { color: theme.accent, children: "\u2588" })
8825
+ ] }) : /* @__PURE__ */ jsx4(Box4, { width: bodyWidth, children: active && isCustomSelected ? /* @__PURE__ */ jsxs3(Fragment, { children: [
8826
+ /* @__PURE__ */ jsx4(Text4, { inverse: true, bold: true, color: theme.accent, children: " \u203A " }),
8827
+ /* @__PURE__ */ jsx4(Text4, { inverse: true, bold: true, color: theme.muted, children: " Custom answer... " })
8828
+ ] }) : /* @__PURE__ */ jsxs3(Fragment, { children: [
8829
+ /* @__PURE__ */ jsx4(Text4, { color: theme.muted, children: " " }),
8830
+ /* @__PURE__ */ jsx4(Text4, { color: theme.muted, dimColor: true, children: "Custom answer..." })
8831
+ ] }) })
8832
+ ] }),
8833
+ /* @__PURE__ */ jsx4(Box4, { marginLeft: 2, marginTop: 0, width: bodyWidth, children: /* @__PURE__ */ jsx4(Text4, { color: theme.muted, dimColor: true, wrap: "wrap", children: mode === "typing" ? "Type your answer \xB7 Enter submit \xB7 Esc back" : active ? "\u2191/\u2193 select \xB7 Enter confirm" : "Waiting..." }) })
8465
8834
  ]
8466
8835
  }
8467
8836
  );
@@ -8656,22 +9025,22 @@ function FooterBar({
8656
9025
  var STREAM_FLUSH_PATTERN = /[.!?]\s|\n|^#{1,3}\s|^[-*]\s/m;
8657
9026
  var STREAM_FLUSH_INTERVAL_MS = 80;
8658
9027
  function splitMessagesForRender(messages, busy) {
8659
- if (!busy || messages.length === 0) {
9028
+ if (messages.length === 0) {
8660
9029
  return {
8661
9030
  staticMessages: messages,
8662
9031
  dynamicMessages: []
8663
9032
  };
8664
9033
  }
8665
9034
  const last = messages[messages.length - 1];
8666
- if (!last || last.role !== "assistant") {
9035
+ if (last && (last.role === "assistant" ? busy : !busy && last.role === "system")) {
8667
9036
  return {
8668
- staticMessages: messages,
8669
- dynamicMessages: []
9037
+ staticMessages: messages.slice(0, -1),
9038
+ dynamicMessages: [last]
8670
9039
  };
8671
9040
  }
8672
9041
  return {
8673
- staticMessages: messages.slice(0, -1),
8674
- dynamicMessages: [last]
9042
+ staticMessages: messages,
9043
+ dynamicMessages: []
8675
9044
  };
8676
9045
  }
8677
9046
  function createSentenceStreamBuffer({
@@ -8720,10 +9089,10 @@ function createSentenceStreamBuffer({
8720
9089
  }
8721
9090
 
8722
9091
  // src/tui/hooks/use-animated-frame.ts
8723
- import { useState as useState4, useEffect as useEffect2 } from "react";
9092
+ import { useState as useState5, useEffect as useEffect2 } from "react";
8724
9093
  var SPINNER_FRAMES = ["\u25D0", "\u25D3", "\u25D1", "\u25D2"];
8725
9094
  function useAnimatedFrame(active) {
8726
- const [index, setIndex] = useState4(0);
9095
+ const [index, setIndex] = useState5(0);
8727
9096
  useEffect2(() => {
8728
9097
  if (!active) {
8729
9098
  setIndex(0);
@@ -8736,28 +9105,49 @@ function useAnimatedFrame(active) {
8736
9105
  }
8737
9106
 
8738
9107
  // src/tui/hooks/use-terminal-width.ts
8739
- import { useState as useState5, useEffect as useEffect3 } from "react";
9108
+ import { useState as useState6, useEffect as useEffect3 } from "react";
8740
9109
  import { useStdout as useStdout2 } from "ink";
9110
+ var RESIZE_DEBOUNCE_MS = 50;
8741
9111
  function useTerminalWidth() {
8742
9112
  const { stdout } = useStdout2();
8743
- const [terminalWidth, setTerminalWidth] = useState5(
9113
+ const [terminalWidth, setTerminalWidth] = useState6(
8744
9114
  () => getObservedTerminalWidth(stdout.columns, process.stdout.columns)
8745
9115
  );
8746
9116
  useEffect3(() => {
8747
9117
  const stream = stdout;
8748
- const updateWidth = () => {
8749
- const nextWidth = getObservedTerminalWidth(stream.columns, process.stdout.columns);
8750
- setTerminalWidth((current) => current === nextWidth ? current : nextWidth);
9118
+ let resizeTimer = null;
9119
+ const commitWidth = () => {
9120
+ setTerminalWidth((current) => {
9121
+ const nextWidth = getStableObservedTerminalWidth(current, stream.columns, process.stdout.columns);
9122
+ return current === nextWidth ? current : nextWidth;
9123
+ });
8751
9124
  };
8752
- updateWidth();
9125
+ const scheduleWidthUpdate = () => {
9126
+ if (resizeTimer) {
9127
+ clearTimeout(resizeTimer);
9128
+ }
9129
+ resizeTimer = setTimeout(() => {
9130
+ resizeTimer = null;
9131
+ commitWidth();
9132
+ }, RESIZE_DEBOUNCE_MS);
9133
+ };
9134
+ commitWidth();
8753
9135
  if (typeof stream.on === "function") {
8754
- stream.on("resize", updateWidth);
9136
+ stream.on("resize", scheduleWidthUpdate);
8755
9137
  return () => {
9138
+ if (resizeTimer) {
9139
+ clearTimeout(resizeTimer);
9140
+ }
8756
9141
  if (typeof stream.off === "function") {
8757
- stream.off("resize", updateWidth);
9142
+ stream.off("resize", scheduleWidthUpdate);
8758
9143
  }
8759
9144
  };
8760
9145
  }
9146
+ return () => {
9147
+ if (resizeTimer) {
9148
+ clearTimeout(resizeTimer);
9149
+ }
9150
+ };
8761
9151
  }, [stdout]);
8762
9152
  return terminalWidth;
8763
9153
  }
@@ -9936,22 +10326,36 @@ function parseCharterYaml(raw) {
9936
10326
 
9937
10327
  // src/tui/helpers/render-message.tsx
9938
10328
  import { jsx as jsx5 } from "react/jsx-runtime";
9939
- function renderConversationMessage(message, key, expanded = false, width) {
9940
- if (message.role === "system") {
9941
- if (message.text.startsWith("__tool_summary__")) {
9942
- try {
9943
- const data = JSON.parse(message.text.slice("__tool_summary__".length));
9944
- return /* @__PURE__ */ jsx5(ToolActivitySummary, { summary: data.summary, tools: data.tools, expanded, width }, key);
9945
- } catch {
9946
- return null;
10329
+ function renderConversationMessages(messages, expanded, width) {
10330
+ const elements = [];
10331
+ let turnCount = 0;
10332
+ for (let i = 0; i < messages.length; i++) {
10333
+ const message = messages[i];
10334
+ const key = `msg-${i}`;
10335
+ if (message.role === "user" && i > 0) {
10336
+ const prev = messages[i - 1];
10337
+ if (prev && prev.role !== "user") {
10338
+ elements.push(/* @__PURE__ */ jsx5(Divider, { width }, `div-${i}`));
10339
+ }
10340
+ }
10341
+ if (message.role === "user") {
10342
+ turnCount++;
10343
+ elements.push(/* @__PURE__ */ jsx5(UserMessage, { text: message.text, turnNumber: turnCount, width }, key));
10344
+ } else if (message.role === "system") {
10345
+ if (message.text.startsWith("__tool_summary__")) {
10346
+ try {
10347
+ const data = JSON.parse(message.text.slice("__tool_summary__".length));
10348
+ elements.push(/* @__PURE__ */ jsx5(ToolActivitySummary, { summary: data.summary, tools: data.tools, expanded, width }, key));
10349
+ } catch {
10350
+ }
10351
+ } else {
10352
+ elements.push(/* @__PURE__ */ jsx5(SystemMessage, { text: message.text, width }, key));
9947
10353
  }
10354
+ } else {
10355
+ elements.push(/* @__PURE__ */ jsx5(AgentMessage, { text: message.text, width }, key));
9948
10356
  }
9949
- return /* @__PURE__ */ jsx5(SystemMessage, { text: message.text, width }, key);
9950
- }
9951
- if (message.role === "user") {
9952
- return /* @__PURE__ */ jsx5(UserMessage, { text: message.text, width }, key);
9953
10357
  }
9954
- return /* @__PURE__ */ jsx5(AgentMessage, { text: message.text, width }, key);
10358
+ return elements;
9955
10359
  }
9956
10360
 
9957
10361
  // src/tui/app.tsx
@@ -9962,55 +10366,84 @@ function App({
9962
10366
  }) {
9963
10367
  const app = useApp();
9964
10368
  const abortRef = useRef2(null);
9965
- const [input2, setInput] = useState6("");
9966
- const [composerFocused, setComposerFocused] = useState6(true);
9967
- const [busy, setBusy] = useState6(false);
9968
- const [authStatus, setAuthStatus] = useState6(initialState.authStatus);
9969
- const [workspacePath, setWorkspacePath] = useState6(initialState.workspacePath);
9970
- const [workspaceFiles, setWorkspaceFiles] = useState6([]);
9971
- const [skills2, setSkills] = useState6([]);
9972
- const [messages, setMessages] = useState6([]);
9973
- const [messageRenderVersion, setMessageRenderVersion] = useState6(0);
9974
- const [history, setHistory] = useState6([]);
9975
- const [activeSkills, setActiveSkills] = useState6([]);
9976
- const [pendingUpdates, setPendingUpdates] = useState6(initialState.pendingUpdates);
9977
- const [statusLine, setStatusLine] = useState6("");
9978
- const [currentToolActivity, setCurrentToolActivity] = useState6("");
9979
- const [turnToolCount, setTurnToolCount] = useState6(0);
9980
- const [subAgentProgress, setSubAgentProgress] = useState6(null);
9981
- const [toolActivityExpanded, setToolActivityExpanded] = useState6(false);
9982
- const [taskPanelVisible, setTaskPanelVisible] = useState6(true);
9983
- const [taskVersion, setTaskVersion] = useState6(0);
10369
+ const [input2, setInput] = useState7("");
10370
+ const [composerFocused, setComposerFocused] = useState7(true);
10371
+ const [busy, setBusy] = useState7(false);
10372
+ const [authStatus, setAuthStatus] = useState7(initialState.authStatus);
10373
+ const [workspacePath, setWorkspacePath] = useState7(initialState.workspacePath);
10374
+ const [workspaceFiles, setWorkspaceFiles] = useState7([]);
10375
+ const [skills2, setSkills] = useState7([]);
10376
+ const [messages, setMessages] = useState7([]);
10377
+ const [history, setHistory] = useState7([]);
10378
+ const [activeSkills, setActiveSkills] = useState7([]);
10379
+ const [pendingUpdates, setPendingUpdates] = useState7(initialState.pendingUpdates);
10380
+ const [statusLine, setStatusLine] = useState7("");
10381
+ const [activeToolActivities, setActiveToolActivities] = useState7({});
10382
+ const [turnToolCount, setTurnToolCount] = useState7(0);
10383
+ const [subAgentProgress, setSubAgentProgress] = useState7({});
10384
+ const [toolActivityExpanded, setToolActivityExpanded] = useState7(false);
10385
+ const [taskPanelVisible, setTaskPanelVisible] = useState7(true);
10386
+ const [taskVersion, setTaskVersion] = useState7(0);
9984
10387
  const turnToolLogRef = useRef2([]);
9985
- const [sessionTokens, setSessionTokens] = useState6(() => createSessionUsage());
9986
- const [tokenDisplay, setTokenDisplay] = useState6("");
9987
- const [showSuggestions, setShowSuggestions] = useState6(false);
9988
- const [agentMode, setAgentMode] = useState6("manual-review");
9989
- const [planningState, setPlanningState] = useState6({
10388
+ const [sessionTokens, setSessionTokens] = useState7(() => createSessionUsage());
10389
+ const [tokenDisplay, setTokenDisplay] = useState7("");
10390
+ const [showSuggestions, setShowSuggestions] = useState7(false);
10391
+ const [agentMode, setAgentMode] = useState7("manual-review");
10392
+ const [planningState, setPlanningState] = useState7({
9990
10393
  status: "idle",
9991
10394
  planningHistory: []
9992
10395
  });
9993
- const [theme, setTheme] = useState6("dark");
9994
- const [config, setConfig] = useState6(null);
9995
- const [cursorToEnd, setCursorToEnd] = useState6(0);
9996
- const [screen, setScreen] = useState6("main");
9997
- const [resumeSessions, setResumeSessions] = useState6([]);
9998
- const [ctrlCPending, setCtrlCPending] = useState6(false);
9999
- const sessionId2 = useMemo3(() => crypto.randomUUID(), []);
10000
- const deferredMessages = useDeferredValue(messages);
10396
+ const [theme, setTheme] = useState7("dark");
10397
+ const [config, setConfig] = useState7(null);
10398
+ const [cursorToEnd, setCursorToEnd] = useState7(0);
10399
+ const [messageRenderVersion, setMessageRenderVersion] = useState7(0);
10400
+ const [screen, setScreen] = useState7("main");
10401
+ const [resumeSessions, setResumeSessions] = useState7([]);
10402
+ const [ctrlCPending, setCtrlCPending] = useState7(false);
10403
+ const sessionId2 = useMemo4(() => crypto.randomUUID(), []);
10001
10404
  const deferredPendingUpdates = useDeferredValue(pendingUpdates);
10405
+ const visiblePendingUpdates = deferredPendingUpdates.length > 0 ? deferredPendingUpdates : pendingUpdates;
10002
10406
  const activityFrame = useAnimatedFrame(busy);
10003
10407
  const terminalWidth = useTerminalWidth();
10004
10408
  const contentWidth = insetWidth(terminalWidth, 2);
10005
10409
  const panelInnerWidth = insetWidth(contentWidth, 4);
10006
10410
  const panelBodyWidth = insetWidth(panelInnerWidth, 2);
10007
- const [agentQuestion, setAgentQuestion] = useState6(null);
10411
+ const [agentQuestion, setAgentQuestion] = useState7(null);
10008
10412
  const previewRef = useRef2(null);
10009
10413
  const ctrlCTimerRef = useRef2(null);
10010
- const isHome = deferredMessages.length === 0 && !busy;
10011
- const { staticMessages, dynamicMessages } = useMemo3(
10012
- () => splitMessagesForRender(deferredMessages, busy),
10013
- [busy, deferredMessages]
10414
+ const isHome = messages.length === 0 && !busy;
10415
+ const { staticMessages, dynamicMessages } = useMemo4(
10416
+ () => splitMessagesForRender(messages, busy),
10417
+ [busy, messages]
10418
+ );
10419
+ const staticRenderItems = useMemo4(
10420
+ () => renderConversationMessages(staticMessages, toolActivityExpanded, contentWidth),
10421
+ [contentWidth, staticMessages, toolActivityExpanded]
10422
+ );
10423
+ const dynamicRenderItems = useMemo4(
10424
+ () => renderConversationMessages(dynamicMessages, toolActivityExpanded, contentWidth),
10425
+ [contentWidth, dynamicMessages, toolActivityExpanded]
10426
+ );
10427
+ const showThinking = busy && (messages.length === 0 || messages[messages.length - 1]?.role !== "assistant");
10428
+ const activeToolDescriptions = useMemo4(
10429
+ () => Object.values(activeToolActivities),
10430
+ [activeToolActivities]
10431
+ );
10432
+ const currentToolActivity = useMemo4(() => {
10433
+ if (activeToolDescriptions.length === 0) {
10434
+ return "";
10435
+ }
10436
+ if (activeToolDescriptions.length === 1 && turnToolCount === 0) {
10437
+ return activeToolDescriptions[0] ?? "";
10438
+ }
10439
+ if (activeToolDescriptions.length === 1) {
10440
+ return "Running 1 tool";
10441
+ }
10442
+ return `Running ${activeToolDescriptions.length} tools in parallel`;
10443
+ }, [activeToolDescriptions, turnToolCount]);
10444
+ const visibleSubAgents = useMemo4(
10445
+ () => Object.values(subAgentProgress),
10446
+ [subAgentProgress]
10014
10447
  );
10015
10448
  const hasWorkspace = workspacePath !== null;
10016
10449
  const hasAuth = authStatus === "connected";
@@ -10023,7 +10456,7 @@ function App({
10023
10456
  const pending = getPendingQuestion();
10024
10457
  if (pending && (!agentQuestion || pending.question.id !== agentQuestion.question.id)) {
10025
10458
  setAgentQuestion(pending);
10026
- setComposerFocused(true);
10459
+ setComposerFocused(false);
10027
10460
  }
10028
10461
  }, 200);
10029
10462
  return () => clearInterval(interval);
@@ -10078,15 +10511,16 @@ function App({
10078
10511
  }
10079
10512
  };
10080
10513
  }, []);
10081
- const [selectedSuggestion, setSelectedSuggestion] = useState6(-1);
10082
- const atMention = useMemo3(() => extractAtMention(input2), [input2]);
10083
- const suggestions = useMemo3(() => {
10514
+ const [selectedSuggestion, setSelectedSuggestion] = useState7(-1);
10515
+ const atMention = useMemo4(() => extractAtMention(input2), [input2]);
10516
+ const slashTrigger = useMemo4(() => extractSlashTrigger(input2), [input2]);
10517
+ const suggestions = useMemo4(() => {
10084
10518
  if (atMention) {
10085
10519
  return getFileSuggestions(atMention.partial, workspaceFiles);
10086
10520
  }
10087
- if (!input2.startsWith("/")) return [];
10088
- return getUnifiedSuggestions(input2, skills2);
10089
- }, [input2, skills2, atMention, workspaceFiles]);
10521
+ if (!slashTrigger) return [];
10522
+ return getUnifiedSuggestions(`/${slashTrigger.partial}`, skills2);
10523
+ }, [input2, skills2, atMention, slashTrigger, workspaceFiles]);
10090
10524
  useEffect4(() => {
10091
10525
  setSelectedSuggestion(-1);
10092
10526
  }, [suggestions.length, input2]);
@@ -10095,8 +10529,9 @@ function App({
10095
10529
  if (s.kind === "file" && atMention) {
10096
10530
  const before = input2.slice(0, atMention.start);
10097
10531
  setInput(`${before}@${s.path} `);
10098
- } else if (s.kind === "command" || s.kind === "skill") {
10099
- setInput(`/${s.name}`);
10532
+ } else if ((s.kind === "command" || s.kind === "skill") && slashTrigger) {
10533
+ const before = input2.slice(0, slashTrigger.start);
10534
+ setInput(`${before}/${s.name}`);
10100
10535
  }
10101
10536
  setSelectedSuggestion(-1);
10102
10537
  setCursorToEnd((c) => c + 1);
@@ -10153,10 +10588,8 @@ function App({
10153
10588
  });
10154
10589
  }
10155
10590
  function replaceMessages(nextMessages) {
10156
- startTransition2(() => {
10157
- setMessageRenderVersion((current) => current + 1);
10158
- setMessages(nextMessages);
10159
- });
10591
+ setMessages(nextMessages);
10592
+ setMessageRenderVersion((current) => current + 1);
10160
10593
  }
10161
10594
  const slashCtx = {
10162
10595
  homeDir,
@@ -10220,6 +10653,19 @@ function App({
10220
10653
  addSystemMessage(`Rejected: ${next.summary}`);
10221
10654
  });
10222
10655
  }
10656
+ function feedbackOnPendingUpdate(feedback) {
10657
+ if (pendingUpdates.length === 0 || !workspacePath) return;
10658
+ const [next, ...rest] = pendingUpdates;
10659
+ void appendSessionEvent(workspacePath, sessionId2, {
10660
+ type: "update.rejected",
10661
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
10662
+ payload: { key: next.key, summary: next.summary, feedback }
10663
+ });
10664
+ startTransition2(() => {
10665
+ setPendingUpdates(rest);
10666
+ addSystemMessage(`Feedback on "${next.summary}": ${feedback}`);
10667
+ });
10668
+ }
10223
10669
  function clearCtrlCPending() {
10224
10670
  if (ctrlCTimerRef.current) {
10225
10671
  clearTimeout(ctrlCTimerRef.current);
@@ -10295,7 +10741,7 @@ function App({
10295
10741
  setPlanningState({ status: "idle", planningHistory: [] });
10296
10742
  addSystemMessage("Charter cancelled. Planning reset.");
10297
10743
  }
10298
- useInput4((key, inputKey) => {
10744
+ useInput5((key, inputKey) => {
10299
10745
  if (inputKey.ctrl && key === "c") {
10300
10746
  if (busy) {
10301
10747
  clearCtrlCPending();
@@ -10368,14 +10814,7 @@ function App({
10368
10814
  return;
10369
10815
  }
10370
10816
  }
10371
- if (key === "a" && pendingUpdates.length > 0) {
10372
- void acceptNextPendingUpdate();
10373
- return;
10374
- }
10375
- if (key === "r" && pendingUpdates.length > 0) {
10376
- rejectNextPendingUpdate();
10377
- return;
10378
- }
10817
+ if (pendingUpdates.length > 0 && !agentQuestion || agentQuestion) return;
10379
10818
  if (key === "i" || key.length === 1 && !inputKey.ctrl && !inputKey.meta && !inputKey.tab) {
10380
10819
  setComposerFocused(true);
10381
10820
  if (key !== "i") setInput((c) => c + key);
@@ -10387,24 +10826,9 @@ function App({
10387
10826
  if (handleDropdownSelect()) return;
10388
10827
  const trimmed = value.trim();
10389
10828
  if (!trimmed) return;
10390
- if (agentQuestion) {
10391
- const options = agentQuestion.question.options;
10392
- const numChoice = parseInt(trimmed, 10);
10393
- if (!isNaN(numChoice) && numChoice >= 1 && numChoice <= options.length) {
10394
- const picked = options[numChoice - 1];
10395
- addSystemMessage(`> ${picked.label}`);
10396
- agentQuestion.resolve({ questionId: agentQuestion.question.id, answer: picked.label, isCustom: false });
10397
- } else {
10398
- addSystemMessage(`> ${trimmed}`);
10399
- agentQuestion.resolve({ questionId: agentQuestion.question.id, answer: trimmed, isCustom: true });
10400
- }
10401
- clearPendingQuestion();
10402
- setAgentQuestion(null);
10403
- setInput("");
10404
- return;
10405
- }
10829
+ if (agentQuestion) return;
10406
10830
  if (busy) return;
10407
- if (trimmed.startsWith("/") && !trimmed.includes(" ") && applyAutocomplete()) {
10831
+ if (dropdownVisible && applyAutocomplete()) {
10408
10832
  return;
10409
10833
  }
10410
10834
  setInput("");
@@ -10447,9 +10871,11 @@ function App({
10447
10871
  if (!workspacePath) return;
10448
10872
  turnToolLogRef.current = [];
10449
10873
  setTurnToolCount(0);
10450
- setSubAgentProgress(null);
10874
+ setActiveToolActivities({});
10875
+ setSubAgentProgress({});
10451
10876
  const controller = new AbortController();
10452
10877
  let streamBuffer = null;
10878
+ let focusPendingReviewOnComplete = false;
10453
10879
  abortRef.current = controller;
10454
10880
  setBusy(true);
10455
10881
  startTransition2(() => {
@@ -10489,9 +10915,16 @@ function App({
10489
10915
  onToolActivity: (activity) => {
10490
10916
  streamBuffer?.flush();
10491
10917
  if (activity.type === "tool_start") {
10492
- setCurrentToolActivity(activity.description ?? activity.name);
10918
+ setActiveToolActivities((current) => ({
10919
+ ...current,
10920
+ [activity.toolCallId]: activity.description ?? activity.name
10921
+ }));
10493
10922
  } else {
10494
- setCurrentToolActivity("");
10923
+ setActiveToolActivities((current) => {
10924
+ const next = { ...current };
10925
+ delete next[activity.toolCallId];
10926
+ return next;
10927
+ });
10495
10928
  turnToolLogRef.current.push({
10496
10929
  name: activity.name,
10497
10930
  description: activity.description ?? activity.name,
@@ -10505,9 +10938,16 @@ function App({
10505
10938
  },
10506
10939
  onSubAgentProgress: (progress) => {
10507
10940
  if (progress.status === "done") {
10508
- setSubAgentProgress(null);
10941
+ setSubAgentProgress((current) => {
10942
+ const next = { ...current };
10943
+ delete next[progress.agentId];
10944
+ return next;
10945
+ });
10509
10946
  } else {
10510
- setSubAgentProgress({ ...progress });
10947
+ setSubAgentProgress((current) => ({
10948
+ ...current,
10949
+ [progress.agentId]: { ...progress }
10950
+ }));
10511
10951
  }
10512
10952
  },
10513
10953
  onMemoryExtracted: (mems) => {
@@ -10554,7 +10994,8 @@ function App({
10554
10994
  }
10555
10995
  }
10556
10996
  if (reviewRequired.length > 0) {
10557
- startTransition2(() => setPendingUpdates((c) => [...c, ...reviewRequired]));
10997
+ focusPendingReviewOnComplete = true;
10998
+ setPendingUpdates((current) => [...current, ...reviewRequired]);
10558
10999
  }
10559
11000
  await appendSessionEvent(workspacePath, sessionId2, {
10560
11001
  type: "chat.turn",
@@ -10586,8 +11027,10 @@ ${error.stack}` : String(error)}` }
10586
11027
  } finally {
10587
11028
  streamBuffer?.dispose();
10588
11029
  abortRef.current = null;
11030
+ setActiveToolActivities({});
11031
+ setSubAgentProgress({});
10589
11032
  setBusy(false);
10590
- setComposerFocused(true);
11033
+ setComposerFocused(!focusPendingReviewOnComplete);
10591
11034
  if (controller.signal.aborted) {
10592
11035
  addSystemMessage("Agent interrupted.");
10593
11036
  }
@@ -10694,10 +11137,10 @@ ${error.stack}` : String(error)}` }
10694
11137
  else statusParts.push("no workspace");
10695
11138
  if (skills2.length > 0) statusParts.push(`${skills2.length} skills`);
10696
11139
  statusParts.push(agentMode);
10697
- if (deferredPendingUpdates.length > 0) statusParts.push(`${deferredPendingUpdates.length} pending`);
11140
+ if (pendingUpdates.length > 0) statusParts.push(`${pendingUpdates.length} pending`);
10698
11141
  const themeColors = getThemeColors(theme);
10699
- const statusColor = busy ? themeColors.warning : ctrlCPending ? themeColors.warning : !hasAuth ? themeColors.error : deferredPendingUpdates.length > 0 ? themeColors.pending : themeColors.secondary;
10700
- const configItems = useMemo3(() => [
11142
+ const statusColor = busy ? themeColors.warning : ctrlCPending ? themeColors.warning : !hasAuth ? themeColors.error : pendingUpdates.length > 0 ? themeColors.pending : themeColors.secondary;
11143
+ const configItems = useMemo4(() => [
10701
11144
  { key: "defaults.model", label: "Model", values: [...getAvailableModels()], current: config?.defaults.model ?? "gpt-5.4" },
10702
11145
  { key: "theme", label: "Theme", values: [...themeValues], current: theme },
10703
11146
  { key: "defaults.reasoningEffort", label: "Reasoning effort", values: ["low", "medium", "high", "xhigh"], current: config?.defaults.reasoningEffort ?? "medium" },
@@ -10731,11 +11174,8 @@ ${error.stack}` : String(error)}` }
10731
11174
  onSelect: async (session) => {
10732
11175
  try {
10733
11176
  const restored = await loadSessionHistory(workspacePath, session.id);
10734
- startTransition2(() => {
10735
- setMessageRenderVersion((current) => current + 1);
10736
- setMessages(restored.messages);
10737
- setHistory(restored.llmHistory);
10738
- });
11177
+ replaceMessages(restored.messages);
11178
+ startTransition2(() => setHistory(restored.llmHistory));
10739
11179
  addSystemMessage(`Resumed session (${session.turnCount} turns). Continue where you left off.`);
10740
11180
  } catch (err) {
10741
11181
  addSystemMessage(`Failed: ${err instanceof Error ? err.message : String(err)}`);
@@ -10768,23 +11208,28 @@ ${error.stack}` : String(error)}` }
10768
11208
  width: contentWidth
10769
11209
  }
10770
11210
  ),
10771
- deferredMessages.length > 0 && /* @__PURE__ */ jsxs4(Box5, { flexDirection: "column", marginBottom: 1, width: contentWidth, children: [
10772
- /* @__PURE__ */ jsx6(Static, { items: staticMessages, children: (message, idx) => renderConversationMessage(message, `static-msg-${idx}`, toolActivityExpanded, contentWidth) }, `conversation-static-${messageRenderVersion}-${toolActivityExpanded ? "e" : "c"}`),
10773
- dynamicMessages.map(
10774
- (message, idx) => renderConversationMessage(message, `dynamic-msg-${staticMessages.length + idx}`, toolActivityExpanded, contentWidth)
10775
- )
10776
- ] }),
10777
- subAgentProgress && /* @__PURE__ */ jsx6(
11211
+ staticRenderItems.length > 0 && /* @__PURE__ */ jsx6(
11212
+ Static,
11213
+ {
11214
+ items: staticRenderItems,
11215
+ children: (item, index) => /* @__PURE__ */ jsx6(React6.Fragment, { children: item }, `conversation-static-item-${messageRenderVersion}-${index}`)
11216
+ },
11217
+ `conversation-static-${messageRenderVersion}`
11218
+ ),
11219
+ dynamicRenderItems.length > 0 && /* @__PURE__ */ jsx6(Box5, { flexDirection: "column", marginBottom: 1, width: contentWidth, children: dynamicRenderItems }),
11220
+ showThinking && /* @__PURE__ */ jsx6(ThinkingIndicator, { frame: activityFrame, width: contentWidth }),
11221
+ visibleSubAgents.map((progress) => /* @__PURE__ */ jsx6(
10778
11222
  SubAgentIndicator,
10779
11223
  {
10780
- agentType: subAgentProgress.agentType,
10781
- goal: subAgentProgress.goal,
10782
- currentTool: subAgentProgress.currentTool,
10783
- toolCount: subAgentProgress.toolCount,
11224
+ agentType: progress.agentType,
11225
+ goal: progress.goal,
11226
+ currentTool: progress.currentTool,
11227
+ toolCount: progress.toolCount,
10784
11228
  frame: activityFrame,
10785
11229
  width: contentWidth
10786
- }
10787
- ),
11230
+ },
11231
+ progress.agentId
11232
+ )),
10788
11233
  taskPanelVisible && getVisibleTasks().length > 0 && /* @__PURE__ */ jsx6(
10789
11234
  TaskPanel,
10790
11235
  {
@@ -10793,11 +11238,28 @@ ${error.stack}` : String(error)}` }
10793
11238
  width: contentWidth
10794
11239
  }
10795
11240
  ),
10796
- deferredPendingUpdates.length > 0 && /* @__PURE__ */ jsx6(
11241
+ visiblePendingUpdates.length > 0 && /* @__PURE__ */ jsx6(
10797
11242
  PendingUpdateCard,
10798
11243
  {
10799
- count: deferredPendingUpdates.length,
10800
- summary: truncate3(deferredPendingUpdates[0].summary, Math.max(24, insetWidth(contentWidth, 8))),
11244
+ count: pendingUpdates.length,
11245
+ summary: truncate3(visiblePendingUpdates[0].summary, Math.max(24, insetWidth(contentWidth, 8))),
11246
+ fileName: visiblePendingUpdates[0].key,
11247
+ updateType: visiblePendingUpdates[0].type,
11248
+ oldContent: visiblePendingUpdates[0].oldContent,
11249
+ newContent: visiblePendingUpdates[0].content,
11250
+ active: !composerFocused && !agentQuestion,
11251
+ onAccept: () => {
11252
+ void acceptNextPendingUpdate();
11253
+ if (pendingUpdates.length <= 1) setComposerFocused(true);
11254
+ },
11255
+ onReject: () => {
11256
+ rejectNextPendingUpdate();
11257
+ if (pendingUpdates.length <= 1) setComposerFocused(true);
11258
+ },
11259
+ onFeedback: (fb) => {
11260
+ feedbackOnPendingUpdate(fb);
11261
+ if (pendingUpdates.length <= 1) setComposerFocused(true);
11262
+ },
10801
11263
  width: contentWidth
10802
11264
  }
10803
11265
  ),
@@ -10841,7 +11303,23 @@ ${error.stack}` : String(error)}` }
10841
11303
  ] }) })
10842
11304
  ] }),
10843
11305
  dropdownVisible && /* @__PURE__ */ jsx6(SuggestionDropdown, { width: contentWidth, items: suggestions, selectedIndex: selectedSuggestion }),
10844
- agentQuestion && /* @__PURE__ */ jsx6(QuestionCard, { width: contentWidth, question: agentQuestion.question.question, options: agentQuestion.question.options }),
11306
+ agentQuestion && /* @__PURE__ */ jsx6(
11307
+ QuestionCard,
11308
+ {
11309
+ width: contentWidth,
11310
+ question: agentQuestion.question.question,
11311
+ options: agentQuestion.question.options,
11312
+ active: !composerFocused,
11313
+ onSelect: (answer, isCustom) => {
11314
+ addSystemMessage(`> ${answer}`);
11315
+ agentQuestion.resolve({ questionId: agentQuestion.question.id, answer, isCustom });
11316
+ clearPendingQuestion();
11317
+ const nextQuestion = getPendingQuestion();
11318
+ setAgentQuestion(nextQuestion);
11319
+ setComposerFocused(nextQuestion === null);
11320
+ }
11321
+ }
11322
+ ),
10845
11323
  /* @__PURE__ */ jsx6(Box5, { borderStyle: "round", borderColor: agentQuestion ? themeColors.warning : composerFocused ? themeColors.borderFocused : themeColors.borderDefault, paddingX: 1, flexDirection: "column", width: contentWidth, children: /* @__PURE__ */ jsxs4(Box5, { children: [
10846
11324
  /* @__PURE__ */ jsx6(PromptPrefix, { busy, frame: activityFrame, hasQuestion: !!agentQuestion, mode: agentMode }),
10847
11325
  /* @__PURE__ */ jsx6(
@@ -10857,7 +11335,7 @@ ${error.stack}` : String(error)}` }
10857
11335
  cursorToEnd,
10858
11336
  accentColor: themeColors.accent,
10859
11337
  mutedColor: themeColors.muted,
10860
- placeholder: agentQuestion ? "Type your answer..." : !hasAuth ? "Type /auth or /config apikey" : !hasWorkspace ? "Type /init to create workspace" : busy ? "Draft your next message while the agent works" : "Ask a question or type / for commands"
11338
+ placeholder: agentQuestion ? "Answer in the card above" : !hasAuth ? "Type /auth or /config apikey" : !hasWorkspace ? "Type /init to create workspace" : pendingUpdates.length > 0 && composerFocused ? "Esc to review updates, or keep drafting" : busy ? "Draft your next message while the agent works" : "Ask a question or type / for commands"
10861
11339
  }
10862
11340
  )
10863
11341
  ] }) }),
@@ -10886,6 +11364,35 @@ ${error.stack}` : String(error)}` }
10886
11364
  ] }) });
10887
11365
  }
10888
11366
 
11367
+ // src/tui/ink-stdout.ts
11368
+ function getStableDimension(current, fallback) {
11369
+ return typeof current === "number" && current > 0 ? current : fallback;
11370
+ }
11371
+ function createStableInkStdout(stdout, options) {
11372
+ const forceFullRedraw = options?.forceFullRedraw ?? process.env.OPEN_RESEARCH_FORCE_FULL_REDRAW === "1";
11373
+ let lastRows = getStableDimension(stdout.rows, 24);
11374
+ let lastColumns = getStableDimension(stdout.columns, 80);
11375
+ return new Proxy(stdout, {
11376
+ get(target, prop, receiver) {
11377
+ if (prop === "rows") {
11378
+ if (forceFullRedraw) {
11379
+ return 0;
11380
+ }
11381
+ const rows = Reflect.get(target, prop, receiver);
11382
+ lastRows = getStableDimension(rows, lastRows);
11383
+ return lastRows;
11384
+ }
11385
+ if (prop === "columns") {
11386
+ const columns = Reflect.get(target, prop, receiver);
11387
+ lastColumns = getStableDimension(columns, lastColumns);
11388
+ return lastColumns;
11389
+ }
11390
+ const value = Reflect.get(target, prop, receiver);
11391
+ return typeof value === "function" ? value.bind(target) : value;
11392
+ }
11393
+ });
11394
+ }
11395
+
10889
11396
  // src/cli.ts
10890
11397
  var program = new Command();
10891
11398
  program.name("open-research").version(getPackageVersion()).description("Local-first research CLI powered by OpenAI account auth or API keys.").argument("[workspacePath]", "Optional workspace path to open").action(async (workspacePath) => {
@@ -10894,7 +11401,7 @@ program.name("open-research").version(getPackageVersion()).description("Local-fi
10894
11401
  const project = await loadWorkspaceProject(target);
10895
11402
  const hasProvider = await hasConfiguredProvider();
10896
11403
  render(
10897
- React6.createElement(App, {
11404
+ React7.createElement(App, {
10898
11405
  initialState: {
10899
11406
  authStatus: hasProvider ? "connected" : "missing",
10900
11407
  workspacePath: project ? target : null,
@@ -10902,7 +11409,10 @@ program.name("open-research").version(getPackageVersion()).description("Local-fi
10902
11409
  pendingUpdates: []
10903
11410
  }
10904
11411
  }),
10905
- { exitOnCtrlC: false }
11412
+ {
11413
+ exitOnCtrlC: false,
11414
+ stdout: createStableInkStdout(process.stdout)
11415
+ }
10906
11416
  );
10907
11417
  });
10908
11418
  program.command("init").argument("[workspacePath]").description("Initialize an Open Research workspace.").action(async (workspacePath) => {