open-research 1.0.0 → 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.
package/dist/cli.js CHANGED
@@ -2,7 +2,16 @@
2
2
  import {
3
3
  appendSessionEvent,
4
4
  loadSessionHistory
5
- } from "./chunk-ZUSIRA5S.js";
5
+ } from "./chunk-HRVDYJEC.js";
6
+ import {
7
+ GEMINI_CODE_ASSIST_URL,
8
+ clearStoredAuth,
9
+ loadGeminiAuth,
10
+ loadStoredAuth,
11
+ refreshGeminiAccessToken,
12
+ saveGeminiAuth,
13
+ saveStoredAuth
14
+ } from "./chunk-3GZIDCV2.js";
6
15
  import {
7
16
  ensureOpenResearchConfig,
8
17
  executeFetchUrl,
@@ -15,25 +24,32 @@ import {
15
24
  getOpenAlexApiKey,
16
25
  getSemanticScholarApiKey,
17
26
  loadOpenResearchConfig,
18
- readJsonFile,
19
27
  saveOpenResearchConfig,
20
- themeValues,
28
+ themeValues
29
+ } from "./chunk-3KZN54JZ.js";
30
+ import {
31
+ readJsonFile,
21
32
  writeJsonFile
22
- } from "./chunk-TQSQRNX6.js";
33
+ } from "./chunk-77Q5B5H7.js";
23
34
  import {
24
- getOpenResearchAuthFile,
25
35
  getOpenResearchRoot,
26
36
  getOpenResearchSkillsDir,
27
37
  getWorkspaceMetaDir,
28
38
  getWorkspaceProjectFile,
29
39
  getWorkspaceSessionsDir
30
- } from "./chunk-I5NVYKG7.js";
40
+ } from "./chunk-4HCPHCC2.js";
41
+ import {
42
+ getAvailableModels,
43
+ getDefaultModel,
44
+ getProviderCatalog,
45
+ selectModelForTask
46
+ } from "./chunk-GVEVKDGV.js";
31
47
  import {
32
48
  __require
33
49
  } from "./chunk-3RG5ZIWI.js";
34
50
 
35
51
  // src/cli.ts
36
- import React6 from "react";
52
+ import React7 from "react";
37
53
  import path19 from "path";
38
54
  import { Command } from "commander";
39
55
  import { render } from "ink";
@@ -164,7 +180,7 @@ ${markdown}`;
164
180
  }
165
181
 
166
182
  // src/lib/auth/import-codex.ts
167
- import fs4 from "fs/promises";
183
+ import fs3 from "fs/promises";
168
184
  import path3 from "path";
169
185
 
170
186
  // src/lib/storage/credential-types.ts
@@ -193,30 +209,6 @@ function getBootstrapCredentialValidation() {
193
209
  };
194
210
  }
195
211
 
196
- // src/lib/auth/store.ts
197
- import fs3 from "fs/promises";
198
- var AUTH_FILE_MODE = 384;
199
- async function ensureCliHome(options) {
200
- const root = getOpenResearchRoot(options);
201
- await fs3.mkdir(root, { recursive: true, mode: 448 });
202
- return root;
203
- }
204
- async function saveStoredAuth(auth2, options) {
205
- await ensureCliHome(options);
206
- const authFile = getOpenResearchAuthFile(options);
207
- await writeJsonFile(authFile, auth2, AUTH_FILE_MODE);
208
- await fs3.chmod(authFile, AUTH_FILE_MODE);
209
- return authFile;
210
- }
211
- async function loadStoredAuth(options) {
212
- const authFile = getOpenResearchAuthFile(options);
213
- return readJsonFile(authFile, null);
214
- }
215
- async function clearStoredAuth(options) {
216
- const authFile = getOpenResearchAuthFile(options);
217
- await fs3.rm(authFile, { force: true });
218
- }
219
-
220
212
  // src/lib/auth/openai-oauth.ts
221
213
  import { createHash, randomBytes } from "crypto";
222
214
  var OPENAI_AUTH_URL = "https://auth.openai.com/oauth/authorize";
@@ -309,7 +301,7 @@ async function importCodexAuth(options = {}) {
309
301
  const now = options.now ?? Date.now;
310
302
  const codexAuthPath = options.codexAuthFilePath ?? path3.join(options.homeDir ?? process.env.HOME ?? "", ".codex", "auth.json");
311
303
  const parsed = JSON.parse(
312
- await fs4.readFile(codexAuthPath, "utf8")
304
+ await fs3.readFile(codexAuthPath, "utf8")
313
305
  );
314
306
  if (parsed.auth_mode !== "chatgpt") {
315
307
  throw new Error("Codex is not signed in with OpenAI on this device.");
@@ -478,55 +470,6 @@ async function loginWithBrowser(options) {
478
470
  // src/lib/llm/config.ts
479
471
  var OPENAI_AUTH_ONLY = process.env.OPENAI_AUTH_ONLY !== "false";
480
472
  var CODEX_RESPONSES_URL = process.env.CODEX_RESPONSES_URL ?? "https://chatgpt.com/backend-api/codex/responses";
481
- var OPENAI_VALIDATION_STALE_MS = 15 * 60 * 1e3;
482
-
483
- // src/lib/llm/provider-catalog.ts
484
- var OPENAI_PROVIDER_MODELS = [
485
- "gpt-5.4",
486
- "gpt-5.4-mini",
487
- "o3",
488
- "o4-mini"
489
- ];
490
- var OPENAI_CATALOG = {
491
- family: "openai",
492
- displayName: "OpenAI",
493
- models: OPENAI_PROVIDER_MODELS,
494
- defaultModel: "gpt-5.4",
495
- backgroundModel: "gpt-5.4-mini"
496
- };
497
- function getProviderCatalog(providerKind) {
498
- switch (providerKind) {
499
- case "openai_auth":
500
- case "openai_api_key":
501
- default:
502
- return OPENAI_CATALOG;
503
- }
504
- }
505
- function getAvailableModels(providerKind) {
506
- return getProviderCatalog(providerKind).models;
507
- }
508
- function isSupportedModel(model, providerKind) {
509
- if (!model) return false;
510
- return getAvailableModels(providerKind).includes(model);
511
- }
512
- function getDefaultModel(providerKind) {
513
- return getProviderCatalog(providerKind).defaultModel;
514
- }
515
- function selectModelForTask(providerKind, requestedModel, task) {
516
- const catalog = getProviderCatalog(providerKind);
517
- const selected = isSupportedModel(requestedModel, providerKind) ? requestedModel : catalog.defaultModel;
518
- switch (task) {
519
- case "conversation":
520
- return selected;
521
- case "compaction":
522
- return selected.includes("5.4") ? catalog.backgroundModel : selected;
523
- case "memory":
524
- case "workspace":
525
- return catalog.backgroundModel;
526
- default:
527
- return selected;
528
- }
529
- }
530
473
 
531
474
  // src/lib/llm/openai-connection.ts
532
475
  var VALIDATION_INSTRUCTIONS = "You are validating whether this OpenAI Codex connection can execute a minimal request. Reply with the single word ok.";
@@ -645,44 +588,42 @@ function trimCredential(value) {
645
588
  const trimmed = value?.trim();
646
589
  return trimmed ? trimmed : void 0;
647
590
  }
648
- async function resolveConfiguredProvider(options) {
649
- const [config, stored] = await Promise.all([
650
- loadOpenResearchConfig({ homeDir: options?.homeDir }),
651
- loadStoredAuth({ homeDir: options?.homeDir })
652
- ]);
591
+ function resolveOpenAI(stored, config) {
653
592
  if (stored) {
654
- return {
655
- kind: "openai_auth",
656
- source: "stored_auth",
657
- stored
658
- };
593
+ return { kind: "openai_auth", source: "stored_auth", stored };
659
594
  }
660
595
  const envKey = trimCredential(process.env.OPENAI_API_KEY);
661
- if (envKey) {
662
- return {
663
- kind: "openai_api_key",
664
- source: "env",
665
- apiKey: envKey
666
- };
667
- }
596
+ if (envKey) return { kind: "openai_api_key", source: "env", apiKey: envKey };
668
597
  const providerKey = trimCredential(config?.providers?.openai?.apiKey);
669
- if (providerKey) {
670
- return {
671
- kind: "openai_api_key",
672
- source: "providers.openai.apiKey",
673
- apiKey: providerKey
674
- };
675
- }
598
+ if (providerKey) return { kind: "openai_api_key", source: "providers.openai.apiKey", apiKey: providerKey };
676
599
  const legacyKey = trimCredential(config?.apiKeys?.openai);
677
- if (legacyKey) {
678
- return {
679
- kind: "openai_api_key",
680
- source: "apiKeys.openai",
681
- apiKey: legacyKey
682
- };
683
- }
600
+ if (legacyKey) return { kind: "openai_api_key", source: "apiKeys.openai", apiKey: legacyKey };
684
601
  return null;
685
602
  }
603
+ function resolveGemini(stored, config) {
604
+ if (stored) {
605
+ return { kind: "gemini_auth", source: "stored_auth", stored };
606
+ }
607
+ const envKey = trimCredential(process.env.GEMINI_API_KEY);
608
+ if (envKey) return { kind: "gemini_api_key", source: "env", apiKey: envKey };
609
+ const providerKey = trimCredential(config?.providers?.gemini?.apiKey);
610
+ if (providerKey) return { kind: "gemini_api_key", source: "providers.gemini.apiKey", apiKey: providerKey };
611
+ const legacyKey = trimCredential(config?.apiKeys?.gemini);
612
+ if (legacyKey) return { kind: "gemini_api_key", source: "apiKeys.gemini", apiKey: legacyKey };
613
+ return null;
614
+ }
615
+ async function resolveConfiguredProvider(options) {
616
+ const [config, openaiStored, geminiStored] = await Promise.all([
617
+ loadOpenResearchConfig({ homeDir: options?.homeDir }),
618
+ loadStoredAuth({ homeDir: options?.homeDir }),
619
+ loadGeminiAuth({ homeDir: options?.homeDir })
620
+ ]);
621
+ const activeProvider = config?.activeProvider ?? "openai";
622
+ if (activeProvider === "gemini") {
623
+ return resolveGemini(geminiStored, config) ?? resolveOpenAI(openaiStored, config);
624
+ }
625
+ return resolveOpenAI(openaiStored, config) ?? resolveGemini(geminiStored, config);
626
+ }
686
627
  async function hasConfiguredProvider(options) {
687
628
  return await resolveConfiguredProvider(options) !== null;
688
629
  }
@@ -691,24 +632,22 @@ async function getConfiguredProviderSummary(options) {
691
632
  if (!resolved) {
692
633
  return {
693
634
  connected: false,
694
- message: "No OpenAI credentials configured. Set OPENAI_API_KEY, run /config apikey <key>, or run /auth."
635
+ message: "No credentials configured. Run /auth (OpenAI), /auth-gemini (Google), or /config apikey <key>."
695
636
  };
696
637
  }
697
638
  if (resolved.kind === "openai_auth") {
698
- return {
699
- connected: true,
700
- kind: resolved.kind,
701
- source: resolved.source,
702
- message: "OpenAI account connected."
703
- };
639
+ return { connected: true, kind: resolved.kind, source: resolved.source, message: "OpenAI account connected." };
704
640
  }
705
- const sourceMessage = resolved.source === "env" ? "OPENAI_API_KEY" : resolved.source === "providers.openai.apiKey" ? "providers.openai.apiKey" : "apiKeys.openai";
706
- return {
707
- connected: true,
708
- kind: resolved.kind,
709
- source: resolved.source,
710
- message: `OpenAI API key configured via ${sourceMessage}.`
711
- };
641
+ if (resolved.kind === "openai_api_key") {
642
+ return { connected: true, kind: resolved.kind, source: resolved.source, message: `OpenAI API key configured via ${resolved.source}.` };
643
+ }
644
+ if (resolved.kind === "gemini_auth") {
645
+ return { connected: true, kind: resolved.kind, source: resolved.source, message: `Google account connected (${resolved.stored.tokens.email}).` };
646
+ }
647
+ if (resolved.kind === "gemini_api_key") {
648
+ return { connected: true, kind: resolved.kind, source: resolved.source, message: `Gemini API key configured via ${resolved.source}.` };
649
+ }
650
+ return { connected: false, message: "Unknown provider state." };
712
651
  }
713
652
 
714
653
  // src/lib/auth/status.ts
@@ -743,7 +682,7 @@ async function getAuthStatus(options) {
743
682
  }
744
683
 
745
684
  // src/lib/skills/registry.ts
746
- import fs5 from "fs/promises";
685
+ import fs4 from "fs/promises";
747
686
  import fsSync from "fs";
748
687
  import path4 from "path";
749
688
  import matter from "gray-matter";
@@ -756,13 +695,13 @@ function normalizeSkillName(name) {
756
695
  }
757
696
  async function ensureUserSkillsDir(options) {
758
697
  const dir = getOpenResearchSkillsDir(options);
759
- await fs5.mkdir(dir, { recursive: true });
698
+ await fs4.mkdir(dir, { recursive: true });
760
699
  return dir;
761
700
  }
762
701
  async function readSkillSummary(skillDir, source2) {
763
702
  const skillFile = path4.join(skillDir, "SKILL.md");
764
703
  try {
765
- const raw = await fs5.readFile(skillFile, "utf8");
704
+ const raw = await fs4.readFile(skillFile, "utf8");
766
705
  const parsed = matter(raw);
767
706
  const name = String(parsed.data.name ?? "").trim();
768
707
  const description = String(parsed.data.description ?? "").trim();
@@ -781,7 +720,7 @@ async function readSkillSummary(skillDir, source2) {
781
720
  }
782
721
  }
783
722
  async function listSkillsInDirectory(rootDir, source2) {
784
- const entries = await fs5.readdir(rootDir, { withFileTypes: true }).catch(() => []);
723
+ const entries = await fs4.readdir(rootDir, { withFileTypes: true }).catch(() => []);
785
724
  const results = await Promise.all(
786
725
  entries.filter((entry) => entry.isDirectory()).map((entry) => readSkillSummary(path4.join(rootDir, entry.name), source2))
787
726
  );
@@ -798,7 +737,7 @@ async function listAvailableSkills(options) {
798
737
  async function validateSkillDirectory(input2) {
799
738
  const errors = [];
800
739
  const skillFile = path4.join(input2.skillDir, "SKILL.md");
801
- const raw = await fs5.readFile(skillFile, "utf8").catch(() => "");
740
+ const raw = await fs4.readFile(skillFile, "utf8").catch(() => "");
802
741
  if (!raw) {
803
742
  return { ok: false, errors: ["SKILL.md is missing."] };
804
743
  }
@@ -829,9 +768,9 @@ async function createSkillScaffold(input2) {
829
768
  const skillsDir = await ensureUserSkillsDir({ homeDir: input2.homeDir });
830
769
  const name = normalizeSkillName(input2.name);
831
770
  const skillDir = path4.join(skillsDir, name);
832
- await fs5.mkdir(path4.join(skillDir, "scripts"), { recursive: true });
833
- await fs5.mkdir(path4.join(skillDir, "references"), { recursive: true });
834
- await fs5.mkdir(path4.join(skillDir, "assets"), { recursive: true });
771
+ await fs4.mkdir(path4.join(skillDir, "scripts"), { recursive: true });
772
+ await fs4.mkdir(path4.join(skillDir, "references"), { recursive: true });
773
+ await fs4.mkdir(path4.join(skillDir, "assets"), { recursive: true });
835
774
  const body = `---
836
775
  name: ${name}
837
776
  description: ${input2.description}
@@ -851,7 +790,7 @@ ${input2.examples.map((example) => `- ${example}`).join("\n")}
851
790
 
852
791
  ${input2.workflow}
853
792
  `;
854
- await fs5.writeFile(path4.join(skillDir, "SKILL.md"), body, "utf8");
793
+ await fs4.writeFile(path4.join(skillDir, "SKILL.md"), body, "utf8");
855
794
  return skillDir;
856
795
  }
857
796
 
@@ -889,25 +828,25 @@ function formatDateTime(value) {
889
828
  }
890
829
 
891
830
  // src/lib/cli/version.ts
892
- var PACKAGE_VERSION = "1.0.0";
831
+ var PACKAGE_VERSION = "1.1.0";
893
832
  function getPackageVersion() {
894
833
  return PACKAGE_VERSION;
895
834
  }
896
835
 
897
836
  // src/tui/app.tsx
898
837
  import path18 from "path";
899
- import {
838
+ import React6, {
900
839
  startTransition as startTransition2,
901
840
  useDeferredValue,
902
841
  useEffect as useEffect4,
903
- useMemo as useMemo3,
842
+ useMemo as useMemo4,
904
843
  useRef as useRef2,
905
- useState as useState6
844
+ useState as useState7
906
845
  } from "react";
907
- 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";
908
847
 
909
848
  // src/tui/text-input.tsx
910
- import { useState, useEffect, useRef } from "react";
849
+ import { useState, useEffect, useLayoutEffect, useRef } from "react";
911
850
  import { Box, Text, useInput, useStdout, measureElement } from "ink";
912
851
 
913
852
  // node_modules/chalk/source/vendor/ansi-styles/index.js
@@ -2055,30 +1994,37 @@ function TextInput({
2055
1994
  pasteMapRef.current.clear();
2056
1995
  }
2057
1996
  }, [originalValue]);
2058
- useEffect(() => {
2059
- const fallbackWidth = stdout.columns ?? 0;
2060
- if (!containerRef.current) {
2061
- if (fallbackWidth > 0) {
2062
- setInputWidth((current) => current === fallbackWidth ? current : fallbackWidth);
2063
- }
2064
- return;
2065
- }
2066
- const measuredWidth = measureElement(containerRef.current).width;
2067
- const nextWidth = measuredWidth > 0 ? measuredWidth : fallbackWidth;
2068
- if (nextWidth > 0) {
2069
- setInputWidth((current) => current === nextWidth ? current : nextWidth);
2070
- }
2071
- });
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]);
2072
2011
  function pasteBadge(entry) {
2073
2012
  return applyThemeColor(source_default.dim, accentColor, `[Pasted text #${entry.id} +${entry.lineCount} lines]`);
2074
2013
  }
2075
- function getSlashCommandEnd() {
2076
- if (!originalValue.startsWith("/")) return 0;
2077
- const spaceIdx = originalValue.indexOf(" ");
2078
- 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 };
2079
2023
  }
2080
2024
  function buildRendered() {
2081
- 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;
2082
2028
  if (showCursor && focus) {
2083
2029
  if (originalValue.length === 0) return source_default.inverse(" ");
2084
2030
  const pasteIds2 = [...pasteMapRef.current.keys()].sort((a, b) => a - b);
@@ -2097,12 +2043,12 @@ function TextInput({
2097
2043
  } else if (i2 === cursorOffset) {
2098
2044
  if (char === "\n") {
2099
2045
  result2 += source_default.inverse(" ") + "\n";
2100
- } else if (cmdEnd > 0 && i2 < cmdEnd) {
2046
+ } else if (inCmd(i2)) {
2101
2047
  result2 += applyThemeColor(source_default.inverse, accentColor, char);
2102
2048
  } else {
2103
2049
  result2 += source_default.inverse(char);
2104
2050
  }
2105
- } else if (cmdEnd > 0 && i2 < cmdEnd) {
2051
+ } else if (inCmd(i2)) {
2106
2052
  result2 += applyThemeColor(source_default, accentColor, char);
2107
2053
  } else {
2108
2054
  result2 += char;
@@ -2123,7 +2069,7 @@ function TextInput({
2123
2069
  const entry = pasteIdx < pasteIds.length ? pasteMapRef.current.get(pasteIds[pasteIdx]) : void 0;
2124
2070
  pasteIdx++;
2125
2071
  result += entry ? pasteBadge(entry) : "";
2126
- } else if (cmdEnd > 0 && i < cmdEnd) {
2072
+ } else if (inCmd(i)) {
2127
2073
  result += applyThemeColor(source_default, accentColor, char);
2128
2074
  } else {
2129
2075
  result += char;
@@ -2355,7 +2301,7 @@ function useTheme() {
2355
2301
  }
2356
2302
 
2357
2303
  // src/lib/workspace/scan.ts
2358
- import fs6 from "fs/promises";
2304
+ import fs5 from "fs/promises";
2359
2305
  import path5 from "path";
2360
2306
  var TEXT_EXTENSIONS = /* @__PURE__ */ new Set([
2361
2307
  ".md",
@@ -2386,7 +2332,7 @@ var IGNORED_DIRS = /* @__PURE__ */ new Set([
2386
2332
  ".turbo"
2387
2333
  ]);
2388
2334
  async function walkDir(rootDir, currentDir, out) {
2389
- const entries = await fs6.readdir(currentDir, { withFileTypes: true });
2335
+ const entries = await fs5.readdir(currentDir, { withFileTypes: true });
2390
2336
  for (const entry of entries) {
2391
2337
  if (IGNORED_DIRS.has(entry.name)) {
2392
2338
  continue;
@@ -2404,7 +2350,7 @@ async function walkDir(rootDir, currentDir, out) {
2404
2350
  key: `path:${relativePath}`,
2405
2351
  label: relativePath,
2406
2352
  path: relativePath,
2407
- content: await fs6.readFile(fullPath, "utf8")
2353
+ content: await fs5.readFile(fullPath, "utf8")
2408
2354
  });
2409
2355
  }
2410
2356
  }
@@ -2434,12 +2380,7 @@ var OPENAI_MODEL_MAP = {
2434
2380
  "gpt-5.4": "gpt-5.4",
2435
2381
  "gpt-5.4-mini": "gpt-5.4-mini",
2436
2382
  "openai/gpt-5.4": "gpt-5.4",
2437
- "openai/gpt-5.4-mini": "gpt-5.4-mini",
2438
- "google/gemini-3-flash-preview": "gpt-5.4",
2439
- "google/gemini-3.1-pro": "gpt-5.4",
2440
- "google/gemini-3.1-flash-lite-preview": "gpt-5.4-mini",
2441
- "google/gemini-2.5-flash-lite": "gpt-5.4-mini",
2442
- "google/gemini-2.5-flash": "gpt-5.4-mini"
2383
+ "openai/gpt-5.4-mini": "gpt-5.4-mini"
2443
2384
  };
2444
2385
  function resolveOpenAIModel(model) {
2445
2386
  return OPENAI_MODEL_MAP[model ?? "gpt-5.4"] ?? "gpt-5.4";
@@ -2563,14 +2504,14 @@ function createOpenAIAuthProvider(credentials, onTokenRefresh, onValidationChang
2563
2504
  }
2564
2505
  return creds.accessToken;
2565
2506
  }
2566
- const sessionId = crypto.randomUUID();
2507
+ const sessionId2 = crypto.randomUUID();
2567
2508
  function buildHeaders3(token) {
2568
2509
  const headers = {
2569
2510
  Authorization: `Bearer ${token}`,
2570
2511
  "Content-Type": "application/json",
2571
2512
  originator: "open-research",
2572
2513
  "User-Agent": `open-research/${getPackageVersion()} (${process.platform} ${process.arch})`,
2573
- session_id: sessionId
2514
+ session_id: sessionId2
2574
2515
  };
2575
2516
  if (creds.accountId) {
2576
2517
  headers["ChatGPT-Account-Id"] = creds.accountId;
@@ -3085,12 +3026,478 @@ function createOpenAIAPIKeyProvider(apiKey) {
3085
3026
  };
3086
3027
  }
3087
3028
 
3029
+ // src/lib/llm/gemini-format.ts
3030
+ function convertMessagesToGemini(messages) {
3031
+ const systemParts = [];
3032
+ const rawContents = [];
3033
+ const toolCallNames = /* @__PURE__ */ new Map();
3034
+ for (const msg of messages) {
3035
+ if (msg.role === "assistant" && msg.tool_calls) {
3036
+ for (const tc of msg.tool_calls) {
3037
+ toolCallNames.set(tc.id, tc.function.name);
3038
+ }
3039
+ }
3040
+ }
3041
+ for (const msg of messages) {
3042
+ if (msg.role === "system") {
3043
+ const text = typeof msg.content === "string" ? msg.content : "";
3044
+ if (text.trim()) systemParts.push(text);
3045
+ continue;
3046
+ }
3047
+ if (msg.role === "user") {
3048
+ const text = typeof msg.content === "string" ? msg.content : "";
3049
+ if (text.trim()) {
3050
+ rawContents.push({ role: "user", parts: [{ text }] });
3051
+ }
3052
+ continue;
3053
+ }
3054
+ if (msg.role === "assistant") {
3055
+ const parts = [];
3056
+ const text = typeof msg.content === "string" ? msg.content : "";
3057
+ if (text.trim()) {
3058
+ parts.push({ text });
3059
+ }
3060
+ if (msg.tool_calls) {
3061
+ for (const tc of msg.tool_calls) {
3062
+ let args = {};
3063
+ try {
3064
+ args = JSON.parse(tc.function.arguments);
3065
+ } catch {
3066
+ }
3067
+ parts.push({
3068
+ functionCall: { name: tc.function.name, args },
3069
+ thoughtSignature: "skip_thought_signature_validator"
3070
+ });
3071
+ }
3072
+ }
3073
+ if (parts.length > 0) {
3074
+ rawContents.push({ role: "model", parts });
3075
+ }
3076
+ continue;
3077
+ }
3078
+ if (msg.role === "tool") {
3079
+ const name = msg.tool_call_id ? toolCallNames.get(msg.tool_call_id) ?? "unknown" : "unknown";
3080
+ const content = typeof msg.content === "string" ? msg.content : "";
3081
+ rawContents.push({
3082
+ role: "user",
3083
+ parts: [{ functionResponse: { name, response: { content } } }]
3084
+ });
3085
+ continue;
3086
+ }
3087
+ }
3088
+ const merged = [];
3089
+ for (const content of rawContents) {
3090
+ const last = merged[merged.length - 1];
3091
+ if (last && last.role === content.role) {
3092
+ last.parts.push(...content.parts);
3093
+ } else {
3094
+ merged.push({ role: content.role, parts: [...content.parts] });
3095
+ }
3096
+ }
3097
+ return {
3098
+ systemInstruction: systemParts.length > 0 ? { parts: systemParts.map((text) => ({ text })) } : void 0,
3099
+ contents: merged
3100
+ };
3101
+ }
3102
+ function convertToolsToGemini(tools) {
3103
+ if (!tools || tools.length === 0) return void 0;
3104
+ return {
3105
+ functionDeclarations: tools.map((t) => ({
3106
+ name: t.function.name,
3107
+ description: t.function.description,
3108
+ parameters: t.function.parameters
3109
+ }))
3110
+ };
3111
+ }
3112
+ function mapReasoningEffort(effort, model) {
3113
+ if (!effort || effort === "none") return void 0;
3114
+ if (model.includes("gemini-3")) {
3115
+ const levelMap = {
3116
+ low: "THINKING_LEVEL_LOW",
3117
+ medium: "THINKING_LEVEL_MEDIUM",
3118
+ high: "THINKING_LEVEL_HIGH",
3119
+ xhigh: "THINKING_LEVEL_HIGH"
3120
+ };
3121
+ return {
3122
+ thinkingConfig: {
3123
+ thinkingLevel: levelMap[effort] ?? "THINKING_LEVEL_MEDIUM",
3124
+ includeThoughts: true
3125
+ }
3126
+ };
3127
+ }
3128
+ const budgetMap = {
3129
+ low: 1024,
3130
+ medium: 8192,
3131
+ high: 16384,
3132
+ xhigh: 32768
3133
+ };
3134
+ return {
3135
+ thinkingConfig: {
3136
+ thinkingBudget: budgetMap[effort] ?? 8192,
3137
+ includeThoughts: true
3138
+ }
3139
+ };
3140
+ }
3141
+ function mapJsonSchema(schema) {
3142
+ if (!schema) return void 0;
3143
+ return {
3144
+ responseMimeType: "application/json",
3145
+ responseSchema: schema.schema
3146
+ };
3147
+ }
3148
+
3149
+ // src/lib/llm/providers/gemini-auth.ts
3150
+ var REFRESH_BUFFER_MS = 6e4;
3151
+ var GEMINI_CLI_VERSION = "0.30.0";
3152
+ var sessionId = crypto.randomUUID();
3153
+ function createGeminiAuthProvider(credentials, onTokenRefresh) {
3154
+ let currentToken = credentials.accessToken;
3155
+ let currentExpiry = credentials.expiresAt;
3156
+ let refreshing = null;
3157
+ async function ensureValidToken() {
3158
+ if (Date.now() < currentExpiry - REFRESH_BUFFER_MS) {
3159
+ return currentToken;
3160
+ }
3161
+ if (!refreshing) {
3162
+ refreshing = (async () => {
3163
+ const result = await refreshGeminiAccessToken(credentials.refreshToken);
3164
+ currentToken = result.access_token;
3165
+ currentExpiry = Date.now() + result.expires_in * 1e3;
3166
+ if (result.refresh_token) {
3167
+ credentials.refreshToken = result.refresh_token;
3168
+ }
3169
+ await onTokenRefresh({
3170
+ ...credentials,
3171
+ accessToken: currentToken,
3172
+ expiresAt: currentExpiry
3173
+ });
3174
+ })();
3175
+ }
3176
+ try {
3177
+ await refreshing;
3178
+ } finally {
3179
+ refreshing = null;
3180
+ }
3181
+ return currentToken;
3182
+ }
3183
+ function buildHeaders3(token, model) {
3184
+ return {
3185
+ Authorization: `Bearer ${token}`,
3186
+ "Content-Type": "application/json",
3187
+ "User-Agent": `GeminiCLI/${GEMINI_CLI_VERSION}/${model} (${process.platform}; ${process.arch})`,
3188
+ "x-activity-request-id": Math.random().toString(36).slice(2, 10)
3189
+ };
3190
+ }
3191
+ function buildRequestBody(options, model) {
3192
+ const messages = options.messages;
3193
+ const { systemInstruction, contents } = convertMessagesToGemini(messages);
3194
+ const generationConfig = {};
3195
+ if (options.temperature !== void 0) generationConfig.temperature = options.temperature;
3196
+ if (options.maxTokens !== void 0) generationConfig.maxOutputTokens = options.maxTokens;
3197
+ const thinking = mapReasoningEffort(options.reasoningEffort, model);
3198
+ if (thinking) Object.assign(generationConfig, thinking);
3199
+ if ("jsonSchema" in options && options.jsonSchema) {
3200
+ const schema = mapJsonSchema(options.jsonSchema);
3201
+ if (schema) Object.assign(generationConfig, schema);
3202
+ }
3203
+ const request = {
3204
+ contents,
3205
+ generationConfig,
3206
+ session_id: sessionId
3207
+ };
3208
+ if (systemInstruction) request.systemInstruction = systemInstruction;
3209
+ if ("tools" in options && options.tools) {
3210
+ const geminiTools = convertToolsToGemini(options.tools);
3211
+ if (geminiTools) request.tools = [geminiTools];
3212
+ }
3213
+ return {
3214
+ project: credentials.projectId,
3215
+ model,
3216
+ user_prompt_id: crypto.randomUUID(),
3217
+ request
3218
+ };
3219
+ }
3220
+ async function callLLM(options) {
3221
+ const model = options.model ?? "gemini-3.1-pro-preview";
3222
+ const token = await ensureValidToken();
3223
+ const body = buildRequestBody(options, model);
3224
+ const startTime = Date.now();
3225
+ const response = await fetch(
3226
+ `${GEMINI_CODE_ASSIST_URL}/v1internal:generateContent`,
3227
+ {
3228
+ method: "POST",
3229
+ headers: buildHeaders3(token, model),
3230
+ body: JSON.stringify(body)
3231
+ }
3232
+ );
3233
+ if (!response.ok) {
3234
+ const text = await response.text();
3235
+ throw new Error(`Gemini API error: ${response.status} ${text}`);
3236
+ }
3237
+ const data = await response.json();
3238
+ const inner = data.response ?? data;
3239
+ const parts = inner.candidates?.[0]?.content?.parts ?? [];
3240
+ const content = parts.map((p) => p.text ?? "").join("");
3241
+ const usage = inner.usageMetadata ?? {};
3242
+ return {
3243
+ content,
3244
+ model,
3245
+ usage: {
3246
+ promptTokens: usage.promptTokenCount ?? 0,
3247
+ completionTokens: usage.candidatesTokenCount ?? 0,
3248
+ totalTokens: usage.totalTokenCount ?? 0
3249
+ },
3250
+ latencyMs: Date.now() - startTime
3251
+ };
3252
+ }
3253
+ async function* callLLMStreaming(options) {
3254
+ const model = options.model ?? "gemini-3.1-pro-preview";
3255
+ const token = await ensureValidToken();
3256
+ const body = buildRequestBody(options, model);
3257
+ const response = await fetch(
3258
+ `${GEMINI_CODE_ASSIST_URL}/v1internal:streamGenerateContent?alt=sse`,
3259
+ {
3260
+ method: "POST",
3261
+ headers: {
3262
+ ...buildHeaders3(token, model),
3263
+ Accept: "text/event-stream"
3264
+ },
3265
+ body: JSON.stringify(body),
3266
+ signal: options.signal
3267
+ }
3268
+ );
3269
+ if (!response.ok) {
3270
+ const text = await response.text();
3271
+ throw new Error(`Gemini streaming error: ${response.status} ${text}`);
3272
+ }
3273
+ const reader = response.body?.getReader();
3274
+ if (!reader) throw new Error("No response body");
3275
+ const decoder = new TextDecoder();
3276
+ let buffer = "";
3277
+ let fullText = "";
3278
+ const toolCalls = [];
3279
+ let toolCallIndex = 0;
3280
+ let lastUsage;
3281
+ while (true) {
3282
+ const { done, value } = await reader.read();
3283
+ if (done) break;
3284
+ buffer += decoder.decode(value, { stream: true });
3285
+ while (buffer.includes("\n")) {
3286
+ const lineEnd = buffer.indexOf("\n");
3287
+ const line = buffer.slice(0, lineEnd).trim();
3288
+ buffer = buffer.slice(lineEnd + 1);
3289
+ if (!line.startsWith("data: ")) continue;
3290
+ const jsonStr = line.slice(6);
3291
+ if (jsonStr === "[DONE]") continue;
3292
+ let parsed;
3293
+ try {
3294
+ parsed = JSON.parse(jsonStr);
3295
+ } catch {
3296
+ continue;
3297
+ }
3298
+ const inner = parsed.response ?? parsed;
3299
+ const candidates = inner.candidates ?? [];
3300
+ const candidate = candidates[0];
3301
+ if (!candidate) continue;
3302
+ const parts = candidate.content?.parts ?? [];
3303
+ for (const part of parts) {
3304
+ if (typeof part.text === "string" && part.text) {
3305
+ if (part.thought === true) continue;
3306
+ fullText += part.text;
3307
+ yield { type: "text_delta", content: part.text };
3308
+ }
3309
+ if (part.functionCall) {
3310
+ const fc = part.functionCall;
3311
+ const id = `call_${crypto.randomUUID().slice(0, 8)}`;
3312
+ const args = JSON.stringify(fc.args ?? {});
3313
+ toolCalls.push({ id, name: fc.name, arguments: args });
3314
+ yield { type: "tool_call_start", index: toolCallIndex, id, name: fc.name };
3315
+ yield { type: "tool_call_delta", index: toolCallIndex, arguments: args };
3316
+ toolCallIndex++;
3317
+ }
3318
+ }
3319
+ const usage = inner.usageMetadata;
3320
+ if (usage) {
3321
+ lastUsage = {
3322
+ promptTokens: usage.promptTokenCount ?? 0,
3323
+ completionTokens: usage.candidatesTokenCount ?? 0,
3324
+ totalTokens: usage.totalTokenCount ?? 0,
3325
+ cachedTokens: usage.cachedContentTokenCount ?? 0,
3326
+ reasoningTokens: usage.thoughtsTokenCount ?? 0
3327
+ };
3328
+ }
3329
+ }
3330
+ }
3331
+ yield {
3332
+ type: "done",
3333
+ content: fullText,
3334
+ toolCalls,
3335
+ usage: lastUsage
3336
+ };
3337
+ }
3338
+ return {
3339
+ kind: "gemini_auth",
3340
+ callLLM,
3341
+ callLLMStreaming
3342
+ };
3343
+ }
3344
+
3345
+ // src/lib/llm/providers/gemini-api-key.ts
3346
+ var GEMINI_API_BASE = "https://generativelanguage.googleapis.com/v1beta";
3347
+ function createGeminiAPIKeyProvider(apiKey) {
3348
+ function buildRequestBody(options, model) {
3349
+ const { systemInstruction, contents } = convertMessagesToGemini(options.messages);
3350
+ const generationConfig = {};
3351
+ if (options.temperature !== void 0) generationConfig.temperature = options.temperature;
3352
+ if (options.maxTokens !== void 0) generationConfig.maxOutputTokens = options.maxTokens;
3353
+ const thinking = mapReasoningEffort(options.reasoningEffort, model);
3354
+ if (thinking) Object.assign(generationConfig, thinking);
3355
+ if ("jsonSchema" in options && options.jsonSchema) {
3356
+ const schema = mapJsonSchema(options.jsonSchema);
3357
+ if (schema) Object.assign(generationConfig, schema);
3358
+ }
3359
+ const body = {
3360
+ contents,
3361
+ generationConfig
3362
+ };
3363
+ if (systemInstruction) body.systemInstruction = systemInstruction;
3364
+ if ("tools" in options && options.tools) {
3365
+ const geminiTools = convertToolsToGemini(options.tools);
3366
+ if (geminiTools) body.tools = [geminiTools];
3367
+ }
3368
+ return body;
3369
+ }
3370
+ async function callLLM(options) {
3371
+ const model = options.model ?? "gemini-3-flash-preview";
3372
+ const body = buildRequestBody(options, model);
3373
+ const startTime = Date.now();
3374
+ const response = await fetch(
3375
+ `${GEMINI_API_BASE}/models/${model}:generateContent`,
3376
+ {
3377
+ method: "POST",
3378
+ headers: {
3379
+ "Content-Type": "application/json",
3380
+ "x-goog-api-key": apiKey
3381
+ },
3382
+ body: JSON.stringify(body)
3383
+ }
3384
+ );
3385
+ if (!response.ok) {
3386
+ const text = await response.text();
3387
+ throw new Error(`Gemini API error: ${response.status} ${text}`);
3388
+ }
3389
+ const data = await response.json();
3390
+ const parts = data.candidates?.[0]?.content?.parts ?? [];
3391
+ const content = parts.map((p) => p.text ?? "").join("");
3392
+ const usage = data.usageMetadata ?? {};
3393
+ return {
3394
+ content,
3395
+ model,
3396
+ usage: {
3397
+ promptTokens: usage.promptTokenCount ?? 0,
3398
+ completionTokens: usage.candidatesTokenCount ?? 0,
3399
+ totalTokens: usage.totalTokenCount ?? 0
3400
+ },
3401
+ latencyMs: Date.now() - startTime
3402
+ };
3403
+ }
3404
+ async function* callLLMStreaming(options) {
3405
+ const model = options.model ?? "gemini-3-flash-preview";
3406
+ const body = buildRequestBody(options, model);
3407
+ const response = await fetch(
3408
+ `${GEMINI_API_BASE}/models/${model}:streamGenerateContent?alt=sse`,
3409
+ {
3410
+ method: "POST",
3411
+ headers: {
3412
+ "Content-Type": "application/json",
3413
+ "x-goog-api-key": apiKey,
3414
+ Accept: "text/event-stream"
3415
+ },
3416
+ body: JSON.stringify(body),
3417
+ signal: options.signal
3418
+ }
3419
+ );
3420
+ if (!response.ok) {
3421
+ const text = await response.text();
3422
+ throw new Error(`Gemini streaming error: ${response.status} ${text}`);
3423
+ }
3424
+ const reader = response.body?.getReader();
3425
+ if (!reader) throw new Error("No response body");
3426
+ const decoder = new TextDecoder();
3427
+ let buffer = "";
3428
+ let fullText = "";
3429
+ const toolCalls = [];
3430
+ let toolCallIndex = 0;
3431
+ let lastUsage;
3432
+ while (true) {
3433
+ const { done, value } = await reader.read();
3434
+ if (done) break;
3435
+ buffer += decoder.decode(value, { stream: true });
3436
+ while (buffer.includes("\n")) {
3437
+ const lineEnd = buffer.indexOf("\n");
3438
+ const line = buffer.slice(0, lineEnd).trim();
3439
+ buffer = buffer.slice(lineEnd + 1);
3440
+ if (!line.startsWith("data: ")) continue;
3441
+ const jsonStr = line.slice(6);
3442
+ if (jsonStr === "[DONE]") continue;
3443
+ let parsed;
3444
+ try {
3445
+ parsed = JSON.parse(jsonStr);
3446
+ } catch {
3447
+ continue;
3448
+ }
3449
+ const candidates = parsed.candidates ?? [];
3450
+ const candidate = candidates[0];
3451
+ if (!candidate) continue;
3452
+ const parts = candidate.content?.parts ?? [];
3453
+ for (const part of parts) {
3454
+ if (typeof part.text === "string" && part.text) {
3455
+ if (part.thought === true) continue;
3456
+ fullText += part.text;
3457
+ yield { type: "text_delta", content: part.text };
3458
+ }
3459
+ if (part.functionCall) {
3460
+ const fc = part.functionCall;
3461
+ const id = `call_${crypto.randomUUID().slice(0, 8)}`;
3462
+ const args = JSON.stringify(fc.args ?? {});
3463
+ toolCalls.push({ id, name: fc.name, arguments: args });
3464
+ yield { type: "tool_call_start", index: toolCallIndex, id, name: fc.name };
3465
+ yield { type: "tool_call_delta", index: toolCallIndex, arguments: args };
3466
+ toolCallIndex++;
3467
+ }
3468
+ }
3469
+ const usage = parsed.usageMetadata;
3470
+ if (usage) {
3471
+ lastUsage = {
3472
+ promptTokens: usage.promptTokenCount ?? 0,
3473
+ completionTokens: usage.candidatesTokenCount ?? 0,
3474
+ totalTokens: usage.totalTokenCount ?? 0,
3475
+ cachedTokens: usage.cachedContentTokenCount ?? 0,
3476
+ reasoningTokens: usage.thoughtsTokenCount ?? 0
3477
+ };
3478
+ }
3479
+ }
3480
+ }
3481
+ yield {
3482
+ type: "done",
3483
+ content: fullText,
3484
+ toolCalls,
3485
+ usage: lastUsage
3486
+ };
3487
+ }
3488
+ return {
3489
+ kind: "gemini_api_key",
3490
+ callLLM,
3491
+ callLLMStreaming
3492
+ };
3493
+ }
3494
+
3088
3495
  // src/lib/llm/provider-factory.ts
3089
3496
  async function createProviderFromStoredAuth(options) {
3090
3497
  const resolved = await resolveConfiguredProvider({ homeDir: options?.homeDir });
3091
3498
  if (!resolved) {
3092
3499
  throw new Error(
3093
- "No OpenAI credentials found. Run /auth to connect via OAuth (free), set OPENAI_API_KEY, or run /config apikey <key>."
3500
+ "No credentials found. Run /auth (OpenAI), /auth-gemini (Google), or /config apikey <key>."
3094
3501
  );
3095
3502
  }
3096
3503
  if (resolved.kind === "openai_auth") {
@@ -3117,7 +3524,32 @@ async function createProviderFromStoredAuth(options) {
3117
3524
  }
3118
3525
  );
3119
3526
  }
3120
- return createOpenAIAPIKeyProvider(resolved.apiKey);
3527
+ if (resolved.kind === "openai_api_key") {
3528
+ return createOpenAIAPIKeyProvider(resolved.apiKey);
3529
+ }
3530
+ if (resolved.kind === "gemini_auth") {
3531
+ const stored = resolved.stored;
3532
+ return createGeminiAuthProvider(
3533
+ {
3534
+ accessToken: stored.tokens.access,
3535
+ refreshToken: stored.tokens.refresh,
3536
+ expiresAt: stored.tokens.expires,
3537
+ email: stored.tokens.email,
3538
+ projectId: stored.tokens.projectId
3539
+ },
3540
+ async (newCreds) => {
3541
+ stored.tokens.access = newCreds.accessToken;
3542
+ stored.tokens.refresh = newCreds.refreshToken;
3543
+ stored.tokens.expires = newCreds.expiresAt;
3544
+ stored.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
3545
+ await saveGeminiAuth(stored, { homeDir: options?.homeDir });
3546
+ }
3547
+ );
3548
+ }
3549
+ if (resolved.kind === "gemini_api_key") {
3550
+ return createGeminiAPIKeyProvider(resolved.apiKey);
3551
+ }
3552
+ throw new Error("Unknown provider kind.");
3121
3553
  }
3122
3554
 
3123
3555
  // src/lib/agent/tool-schemas.ts
@@ -3219,14 +3651,24 @@ var TOOL_SCHEMAS = [
3219
3651
  type: "function",
3220
3652
  function: {
3221
3653
  name: "write_new_file",
3222
- 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(" "),
3223
3665
  parameters: {
3224
3666
  type: "object",
3225
3667
  properties: {
3226
- key: { type: "string" },
3227
- 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" },
3228
3670
  content: { type: "string" },
3229
- folder: { type: "string" }
3671
+ folder: { type: "string", description: "Optional subfolder within the managed directory for organization" }
3230
3672
  },
3231
3673
  required: ["key", "label", "content"],
3232
3674
  additionalProperties: false
@@ -3377,32 +3819,56 @@ var TOOL_SCHEMAS = [
3377
3819
  type: "function",
3378
3820
  function: {
3379
3821
  name: "ask_user",
3380
- 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.",
3381
3823
  parameters: {
3382
3824
  type: "object",
3383
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)
3384
3855
  question: {
3385
3856
  type: "string",
3386
- description: "The question to ask the user."
3857
+ description: "Single question (shorthand). Use 'questions' array for multiple."
3387
3858
  },
3388
3859
  options: {
3389
3860
  type: "array",
3390
3861
  items: {
3391
3862
  type: "object",
3392
3863
  properties: {
3393
- label: { type: "string", description: "Short option label (1-5 words)." },
3394
- description: { type: "string", description: "One-sentence description of this option." }
3864
+ label: { type: "string" },
3865
+ description: { type: "string" }
3395
3866
  },
3396
3867
  required: ["label", "description"]
3397
3868
  },
3398
- description: "Predefined options for the user to choose from."
3399
- },
3400
- allow_custom: {
3401
- type: "boolean",
3402
- description: "Whether the user can type a custom answer. Default: true."
3869
+ description: "Options for single question (shorthand)."
3403
3870
  }
3404
3871
  },
3405
- required: ["question"],
3406
3872
  additionalProperties: false
3407
3873
  }
3408
3874
  }
@@ -3668,7 +4134,7 @@ Root: ${process.cwd()}`,
3668
4134
  }
3669
4135
 
3670
4136
  // src/lib/agent/tools/read-file.ts
3671
- import fs7 from "fs/promises";
4137
+ import fs6 from "fs/promises";
3672
4138
  import { createReadStream } from "fs";
3673
4139
  import path6 from "path";
3674
4140
  import os2 from "os";
@@ -3732,7 +4198,7 @@ function isBinaryByExtension(filePath) {
3732
4198
  return BINARY_EXTENSIONS.has(path6.extname(filePath).toLowerCase());
3733
4199
  }
3734
4200
  async function isBinaryByContent(filePath) {
3735
- const handle = await fs7.open(filePath, "r");
4201
+ const handle = await fs6.open(filePath, "r");
3736
4202
  try {
3737
4203
  const buf = Buffer.alloc(4096);
3738
4204
  const { bytesRead } = await handle.read(buf, 0, 4096, 0);
@@ -3797,7 +4263,7 @@ async function executeReadFile(args, ctx) {
3797
4263
  const resolved = path6.isAbsolute(filePath) ? filePath : path6.resolve(filePath);
3798
4264
  let stat;
3799
4265
  try {
3800
- stat = await fs7.stat(resolved);
4266
+ stat = await fs6.stat(resolved);
3801
4267
  } catch {
3802
4268
  if (filePath in ctx.workspaceFiles) {
3803
4269
  return formatFromString(filePath, ctx.workspaceFiles[filePath], offset, limit);
@@ -3805,7 +4271,7 @@ async function executeReadFile(args, ctx) {
3805
4271
  return `Error: File not found: ${args.file_path}`;
3806
4272
  }
3807
4273
  if (stat.isDirectory()) {
3808
- const entries = await fs7.readdir(resolved, { withFileTypes: true });
4274
+ const entries = await fs6.readdir(resolved, { withFileTypes: true });
3809
4275
  const lines = entries.sort((a, b) => {
3810
4276
  if (a.isDirectory() !== b.isDirectory()) return a.isDirectory() ? -1 : 1;
3811
4277
  return a.name.localeCompare(b.name);
@@ -3869,7 +4335,7 @@ ${outputLines.join("\n")}${footer}
3869
4335
  }
3870
4336
 
3871
4337
  // src/lib/agent/tools/list-directory.ts
3872
- import fs8 from "fs/promises";
4338
+ import fs7 from "fs/promises";
3873
4339
  import path7 from "path";
3874
4340
  var MAX_ENTRIES = 200;
3875
4341
  var DEFAULT_IGNORE = /* @__PURE__ */ new Set([
@@ -3887,7 +4353,7 @@ var DEFAULT_IGNORE = /* @__PURE__ */ new Set([
3887
4353
  ".env"
3888
4354
  ]);
3889
4355
  async function listEntries(dirPath, ignore) {
3890
- const entries = await fs8.readdir(dirPath, { withFileTypes: true });
4356
+ const entries = await fs7.readdir(dirPath, { withFileTypes: true });
3891
4357
  const results = [];
3892
4358
  for (const entry of entries) {
3893
4359
  if (ignore.has(entry.name) || entry.name.startsWith(".") && DEFAULT_IGNORE.has(entry.name)) {
@@ -3896,7 +4362,7 @@ async function listEntries(dirPath, ignore) {
3896
4362
  let size = 0;
3897
4363
  if (!entry.isDirectory()) {
3898
4364
  try {
3899
- const stat = await fs8.stat(path7.join(dirPath, entry.name));
4365
+ const stat = await fs7.stat(path7.join(dirPath, entry.name));
3900
4366
  size = stat.size;
3901
4367
  } catch {
3902
4368
  }
@@ -3954,7 +4420,7 @@ async function executeListDirectory(args) {
3954
4420
  const maxDepth = Math.min(args.depth ?? 2, 5);
3955
4421
  const ignore = /* @__PURE__ */ new Set([...DEFAULT_IGNORE, ...args.ignore ?? []]);
3956
4422
  try {
3957
- const stat = await fs8.stat(dirPath);
4423
+ const stat = await fs7.stat(dirPath);
3958
4424
  if (!stat.isDirectory()) {
3959
4425
  return `Error: ${dirPath} is not a directory.`;
3960
4426
  }
@@ -3969,7 +4435,7 @@ async function executeListDirectory(args) {
3969
4435
 
3970
4436
  // src/lib/agent/tools/run-command.ts
3971
4437
  import { spawn } from "child_process";
3972
- import fs9 from "fs/promises";
4438
+ import fs8 from "fs/promises";
3973
4439
  import path8 from "path";
3974
4440
  var DEFAULT_TIMEOUT_MS = 2 * 60 * 1e3;
3975
4441
  var MAX_TIMEOUT_MS = 10 * 60 * 1e3;
@@ -3981,7 +4447,7 @@ async function executeRunCommand(args, signal) {
3981
4447
  }
3982
4448
  const workdir = args.workdir ? path8.isAbsolute(args.workdir) ? args.workdir : path8.resolve(args.workdir) : process.cwd();
3983
4449
  try {
3984
- const stat = await fs9.stat(workdir);
4450
+ const stat = await fs8.stat(workdir);
3985
4451
  if (!stat.isDirectory()) {
3986
4452
  return `Error: workdir is not a directory: ${workdir}`;
3987
4453
  }
@@ -4116,36 +4582,52 @@ function clearPendingQuestion() {
4116
4582
  function resetPendingQuestions() {
4117
4583
  pendingQuestions = [];
4118
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
+ }
4119
4594
  async function executeAskUser(args, signal) {
4120
- const questionId = crypto.randomUUID();
4121
- const question = {
4122
- id: questionId,
4123
- question: args.question,
4124
- options: (args.options ?? []).map((o) => ({
4125
- label: o.label,
4126
- description: o.description
4127
- })),
4128
- allowCustom: args.allow_custom ?? true
4129
- };
4130
- const answerPromise = new Promise((resolve, reject) => {
4131
- pendingQuestions.push({ question, resolve });
4132
- if (signal) {
4133
- const onAbort = () => {
4134
- pendingQuestions = pendingQuestions.filter((q) => q.question.id !== questionId);
4135
- reject(new Error("Question cancelled \u2014 user interrupted."));
4136
- };
4137
- if (signal.aborted) {
4138
- onAbort();
4139
- 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 });
4140
4623
  }
4141
- signal.addEventListener("abort", onAbort, { once: true });
4142
- }
4143
- });
4144
- const answer = await answerPromise;
4145
- if (answer.isCustom) {
4146
- 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
+ );
4147
4629
  }
4148
- return `User selected: "${answer.answer}"`;
4630
+ return answers.join("\n");
4149
4631
  }
4150
4632
 
4151
4633
  // src/lib/agent/tools/search-workspace.ts
@@ -4242,8 +4724,15 @@ function normalizeDbBackedKey(key) {
4242
4724
  }
4243
4725
  return `${prefix}:${crypto.randomUUID()}`;
4244
4726
  }
4727
+ var VALID_PREFIXES = ["note:", "paper:", "experiment:", "source:", "path:"];
4245
4728
  function executeWriteNewFile(args, ctx) {
4246
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
+ }
4247
4736
  if (normalizedKey in ctx.workspaceFiles) {
4248
4737
  return {
4249
4738
  result: `Error: File "${normalizedKey}" already exists. Use update_existing_file to modify it.`,
@@ -4274,6 +4763,7 @@ function executeUpdateExistingFile(args, ctx) {
4274
4763
  };
4275
4764
  }
4276
4765
  const mode = args.mode ?? "rewrite";
4766
+ const oldContent = ctx.workspaceFiles[args.key];
4277
4767
  if (mode === "rewrite") {
4278
4768
  const content = args.content;
4279
4769
  if (content == null) {
@@ -4287,6 +4777,7 @@ function executeUpdateExistingFile(args, ctx) {
4287
4777
  type: "edit",
4288
4778
  key: args.key,
4289
4779
  content,
4780
+ oldContent,
4290
4781
  summary: args.summary
4291
4782
  };
4292
4783
  return {
@@ -4350,6 +4841,7 @@ ${preview}`,
4350
4841
  type: "edit",
4351
4842
  key: args.key,
4352
4843
  content: currentContent,
4844
+ oldContent,
4353
4845
  summary: autoSummary
4354
4846
  };
4355
4847
  return {
@@ -4391,7 +4883,7 @@ function executeCreatePaper(args, ctx) {
4391
4883
 
4392
4884
  // src/lib/agent/tools/read-pdf.ts
4393
4885
  import path9 from "path";
4394
- import fs10 from "fs/promises";
4886
+ import fs9 from "fs/promises";
4395
4887
  var MAX_OUTPUT_BYTES3 = 5e4;
4396
4888
  function parsePages(pages) {
4397
4889
  const range = pages.match(/^(\d+)\s*-\s*(\d+)$/);
@@ -4402,7 +4894,7 @@ function parsePages(pages) {
4402
4894
  }
4403
4895
  async function executeReadPdf(args) {
4404
4896
  const resolved = path9.resolve(args.file_path);
4405
- const stat = await fs10.stat(resolved).catch(() => null);
4897
+ const stat = await fs9.stat(resolved).catch(() => null);
4406
4898
  if (!stat || !stat.isFile()) {
4407
4899
  return `Error: file not found: ${resolved}`;
4408
4900
  }
@@ -5589,7 +6081,7 @@ ${summary}`,
5589
6081
  }
5590
6082
 
5591
6083
  // src/lib/skills/runtime.ts
5592
- import fs11 from "fs/promises";
6084
+ import fs10 from "fs/promises";
5593
6085
  import path10 from "path";
5594
6086
  import matter2 from "gray-matter";
5595
6087
  async function loadRuntimeSkillByName(input2) {
@@ -5598,7 +6090,7 @@ async function loadRuntimeSkillByName(input2) {
5598
6090
  if (!match) {
5599
6091
  return null;
5600
6092
  }
5601
- const raw = await fs11.readFile(path10.join(match.skillDir, "SKILL.md"), "utf8");
6093
+ const raw = await fs10.readFile(path10.join(match.skillDir, "SKILL.md"), "utf8");
5602
6094
  const parsed = matter2(raw);
5603
6095
  return {
5604
6096
  id: match.name,
@@ -5610,14 +6102,15 @@ async function loadRuntimeSkillByName(input2) {
5610
6102
  }
5611
6103
  async function readSkillReferenceFile(skillDir, referencePath) {
5612
6104
  const fullPath = path10.join(skillDir, "references", referencePath);
5613
- return fs11.readFile(fullPath, "utf8");
6105
+ return fs10.readFile(fullPath, "utf8");
5614
6106
  }
5615
6107
 
5616
6108
  // src/lib/agent/subagent/configs.ts
5617
6109
  var exploreConfig = {
5618
6110
  id: "explore",
5619
6111
  name: "Explore",
5620
- model: "gpt-5.4-mini",
6112
+ model: void 0,
6113
+ // Resolved at runtime from provider catalog (backgroundModel)
5621
6114
  reasoningEffort: "high",
5622
6115
  allowedTools: /* @__PURE__ */ new Set([
5623
6116
  "read_file",
@@ -5714,7 +6207,7 @@ ${input2.context}` : input2.goal;
5714
6207
  for await (const chunk of input2.provider.callLLMStreaming({
5715
6208
  messages,
5716
6209
  tools,
5717
- model: config.model,
6210
+ model: config.model ?? getProviderCatalog(input2.provider.kind).backgroundModel,
5718
6211
  reasoningEffort: config.reasoningEffort,
5719
6212
  signal: input2.signal
5720
6213
  })) {
@@ -5775,7 +6268,7 @@ ${input2.context}` : input2.goal;
5775
6268
  let finalText = "";
5776
6269
  for await (const chunk of input2.provider.callLLMStreaming({
5777
6270
  messages,
5778
- model: config.model,
6271
+ model: config.model ?? getProviderCatalog(input2.provider.kind).backgroundModel,
5779
6272
  reasoningEffort: config.reasoningEffort,
5780
6273
  signal: input2.signal
5781
6274
  })) {
@@ -5816,7 +6309,7 @@ function describeSubAgentTool(name, args) {
5816
6309
 
5817
6310
  // src/lib/agent/tools/tasks.ts
5818
6311
  import path11 from "path";
5819
- import fs12 from "fs/promises";
6312
+ import fs11 from "fs/promises";
5820
6313
  var tasks = [];
5821
6314
  var storePath = null;
5822
6315
  function shortId() {
@@ -5826,9 +6319,9 @@ async function persist() {
5826
6319
  if (!storePath) return;
5827
6320
  const live = tasks.filter((t) => t.status !== "deleted");
5828
6321
  const tmpPath = storePath + ".tmp";
5829
- await fs12.mkdir(path11.dirname(storePath), { recursive: true });
5830
- await fs12.writeFile(tmpPath, JSON.stringify({ version: 1, tasks: live }, null, 2));
5831
- await fs12.rename(tmpPath, storePath);
6322
+ await fs11.mkdir(path11.dirname(storePath), { recursive: true });
6323
+ await fs11.writeFile(tmpPath, JSON.stringify({ version: 1, tasks: live }, null, 2));
6324
+ await fs11.rename(tmpPath, storePath);
5832
6325
  }
5833
6326
  async function initTaskStore(workspaceDir) {
5834
6327
  storePath = path11.join(workspaceDir, ".open-research", "tasks.json");
@@ -5960,7 +6453,7 @@ async function executeTool(name, args, ctx, activeSkills, homeDir, signal, provi
5960
6453
  return { result: out.result, searchResults: out.sources };
5961
6454
  }
5962
6455
  case "web_search": {
5963
- const { executeWebSearch } = await import("./web-search-B7D5WMHU.js");
6456
+ const { executeWebSearch } = await import("./web-search-TKBFSU3M.js");
5964
6457
  const out = await executeWebSearch(
5965
6458
  args,
5966
6459
  provider
@@ -6039,7 +6532,7 @@ ${meta}` };
6039
6532
  if (!provider) {
6040
6533
  return { result: "Error: query_ontology requires an LLM provider." };
6041
6534
  }
6042
- const { runQueryAgent } = await import("./query-agent-LRUUJR4F.js");
6535
+ const { runQueryAgent } = await import("./query-agent-WM6UNZ37.js");
6043
6536
  const workspaceDir = ctx.workspaceDir ?? process.cwd();
6044
6537
  const answer = await runQueryAgent({
6045
6538
  query: String(args.query ?? ""),
@@ -6101,7 +6594,9 @@ var MODEL_CONTEXT_WINDOWS = {
6101
6594
  "gpt-4o": 128e3,
6102
6595
  "gpt-5.4-mini": 128e3,
6103
6596
  "o3": 2e5,
6104
- "o4-mini": 2e5
6597
+ "o4-mini": 2e5,
6598
+ "gemini-3.1-pro-preview": 1048576,
6599
+ "gemini-3-flash-preview": 1048576
6105
6600
  };
6106
6601
  var DEFAULT_CONTEXT_WINDOW = 128e3;
6107
6602
  var AUTO_COMPACT_TOKEN_LIMIT = 25e4;
@@ -6315,7 +6810,7 @@ async function manualCompact(messages, model, provider, usage, customInstruction
6315
6810
  }
6316
6811
 
6317
6812
  // src/lib/memory/store.ts
6318
- import fs13 from "fs/promises";
6813
+ import fs12 from "fs/promises";
6319
6814
  import path12 from "path";
6320
6815
  function getGlobalMemoryFile(options) {
6321
6816
  return path12.join(getOpenResearchRoot(options), "memory.json");
@@ -6325,7 +6820,7 @@ function getProjectMemoryFile(workspaceDir) {
6325
6820
  }
6326
6821
  async function loadMemoryFile(filePath) {
6327
6822
  try {
6328
- const raw = await fs13.readFile(filePath, "utf8");
6823
+ const raw = await fs12.readFile(filePath, "utf8");
6329
6824
  const store = JSON.parse(raw);
6330
6825
  return store.memories ?? [];
6331
6826
  } catch {
@@ -6333,9 +6828,9 @@ async function loadMemoryFile(filePath) {
6333
6828
  }
6334
6829
  }
6335
6830
  async function saveMemoryFile(filePath, memories) {
6336
- await fs13.mkdir(path12.dirname(filePath), { recursive: true });
6831
+ await fs12.mkdir(path12.dirname(filePath), { recursive: true });
6337
6832
  const store = { version: 2, memories };
6338
- await fs13.writeFile(filePath, JSON.stringify(store, null, 2), "utf8");
6833
+ await fs12.writeFile(filePath, JSON.stringify(store, null, 2), "utf8");
6339
6834
  }
6340
6835
  async function loadGlobalMemories(options) {
6341
6836
  const mems = await loadMemoryFile(getGlobalMemoryFile(options));
@@ -6667,18 +7162,18 @@ async function extractAndStoreMemories(input2) {
6667
7162
  }
6668
7163
 
6669
7164
  // src/lib/workspace/agents-md.ts
6670
- import fs14 from "fs/promises";
7165
+ import fs13 from "fs/promises";
6671
7166
  import path13 from "path";
6672
7167
  var AGENTS_MD_FILENAME = "AGENTS.md";
6673
7168
  async function readAgentsMd(workspaceDir) {
6674
7169
  try {
6675
- return await fs14.readFile(path13.join(workspaceDir, AGENTS_MD_FILENAME), "utf8");
7170
+ return await fs13.readFile(path13.join(workspaceDir, AGENTS_MD_FILENAME), "utf8");
6676
7171
  } catch {
6677
7172
  return "";
6678
7173
  }
6679
7174
  }
6680
7175
  async function writeAgentsMd(workspaceDir, content) {
6681
- await fs14.writeFile(path13.join(workspaceDir, AGENTS_MD_FILENAME), content, "utf8");
7176
+ await fs13.writeFile(path13.join(workspaceDir, AGENTS_MD_FILENAME), content, "utf8");
6682
7177
  }
6683
7178
  var UPDATE_SYSTEM_PROMPT = `You maintain an AGENTS.md file for a research workspace. This file gives future agent sessions instant context about the project \u2014 what it's about, what's been done, key files, and current direction.
6684
7179
 
@@ -6857,6 +7352,17 @@ ${skill.prompt}`).join("\n\n");
6857
7352
  "- Redirect large outputs to files. Read selectively \u2014 don't dump entire datasets into responses.",
6858
7353
  "- Always wrap file paths in backticks: `notes/brief.md`, `experiments/analysis.py:42`.",
6859
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
+ "",
6860
7366
  `## Workspace
6861
7367
  Root: ${process.cwd()}`,
6862
7368
  skillText
@@ -6881,7 +7387,7 @@ async function runAgentTurn(input2) {
6881
7387
  const isPlanning = input2.mode === "planning";
6882
7388
  const tools = isPlanning ? getToolsForMode("planning") : TOOL_SCHEMAS;
6883
7389
  const systemPrompt = isPlanning ? buildPlanningSystemPrompt(input2.workspace, activeSkills) : buildSystemPrompt(input2.workspace, activeSkills);
6884
- const model = input2.model ?? "gpt-5.4";
7390
+ const model = input2.model ?? getProviderCatalog(input2.provider.kind).defaultModel;
6885
7391
  const usage = input2.sessionUsage ?? createSessionUsage();
6886
7392
  const allMemories = await loadAllMemories({
6887
7393
  homeDir: input2.homeDir,
@@ -6895,7 +7401,7 @@ async function runAgentTurn(input2) {
6895
7401
  if (input2.workspace.workspaceDir) {
6896
7402
  try {
6897
7403
  const { loadOntology } = await import("./store-LT5EGDOI.js");
6898
- const { runRelevanceAgent } = await import("./relevance-agent-CCN7JGTM.js");
7404
+ const { runRelevanceAgent } = await import("./relevance-agent-H3U6TROD.js");
6899
7405
  const { buildScaffoldingContext } = await import("./scaffolding-MSAICMWV.js");
6900
7406
  const ontology = await loadOntology(input2.workspace.workspaceDir);
6901
7407
  const relevantIds = await runRelevanceAgent({
@@ -6996,7 +7502,7 @@ ${agentsMd}` : null,
6996
7502
  });
6997
7503
  }
6998
7504
  if (input2.workspace.workspaceDir) {
6999
- import("./manager-queue-F4VVZMTE.js").then(({ enqueueOntologyManager }) => {
7505
+ import("./manager-queue-FBAUCAGI.js").then(({ enqueueOntologyManager }) => {
7000
7506
  enqueueOntologyManager({
7001
7507
  userMessage: input2.message,
7002
7508
  agentResponse: fullText,
@@ -7155,31 +7661,33 @@ function classifyUpdateRisk(update) {
7155
7661
  }
7156
7662
 
7157
7663
  // src/lib/workspace/apply-update.ts
7158
- import fs15 from "fs/promises";
7664
+ import fs14 from "fs/promises";
7159
7665
  import path14 from "path";
7160
7666
  function resolveRelativePath(update) {
7667
+ const folder = update.folder;
7668
+ const sub = (dir, name, ext) => folder ? `${dir}/${folder}/${name}${ext}` : `${dir}/${name}${ext}`;
7161
7669
  if (update.key.startsWith("path:")) {
7162
7670
  return update.key.slice(5);
7163
7671
  }
7164
7672
  if (update.key.startsWith("note:")) {
7165
- return `notes/${update.key.slice(5)}.md`;
7673
+ return sub("notes", update.key.slice(5), ".md");
7166
7674
  }
7167
7675
  if (update.key.startsWith("paper:")) {
7168
- return `papers/${update.key.slice(6)}.tex`;
7676
+ return sub("papers", update.key.slice(6), ".tex");
7169
7677
  }
7170
7678
  if (update.key.startsWith("experiment:")) {
7171
- return `experiments/${update.key.slice(11)}.json`;
7679
+ return sub("experiments", update.key.slice(11), ".json");
7172
7680
  }
7173
7681
  if (update.key.startsWith("source:")) {
7174
- return `sources/${update.key.slice(7)}.md`;
7682
+ return sub("sources", update.key.slice(7), ".md");
7175
7683
  }
7176
7684
  return `artifacts/${update.key}.md`;
7177
7685
  }
7178
7686
  async function applyProposedUpdate(workspaceDir, update) {
7179
7687
  const relativePath = resolveRelativePath(update);
7180
7688
  const absolutePath = path14.join(workspaceDir, relativePath);
7181
- await fs15.mkdir(path14.dirname(absolutePath), { recursive: true });
7182
- await fs15.writeFile(absolutePath, update.content, "utf8");
7689
+ await fs14.mkdir(path14.dirname(absolutePath), { recursive: true });
7690
+ await fs14.writeFile(absolutePath, update.content, "utf8");
7183
7691
  return absolutePath;
7184
7692
  }
7185
7693
 
@@ -7397,7 +7905,7 @@ function SessionPicker({ sessions, onSelect, onCancel }) {
7397
7905
  }
7398
7906
 
7399
7907
  // src/lib/cli/update-check.ts
7400
- import fs16 from "fs/promises";
7908
+ import fs15 from "fs/promises";
7401
7909
  import path15 from "path";
7402
7910
  import os3 from "os";
7403
7911
  var PACKAGE_NAME = "open-research";
@@ -7405,7 +7913,7 @@ var CHECK_INTERVAL_MS = 4 * 60 * 60 * 1e3;
7405
7913
  var STATE_FILE = path15.join(os3.homedir(), ".open-research", "update-check.json");
7406
7914
  async function readState() {
7407
7915
  try {
7408
- const raw = await fs16.readFile(STATE_FILE, "utf8");
7916
+ const raw = await fs15.readFile(STATE_FILE, "utf8");
7409
7917
  const parsed = JSON.parse(raw);
7410
7918
  return {
7411
7919
  lastCheck: typeof parsed.lastCheck === "number" ? parsed.lastCheck : 0,
@@ -7417,8 +7925,8 @@ async function readState() {
7417
7925
  }
7418
7926
  }
7419
7927
  async function writeState(state) {
7420
- await fs16.mkdir(path15.dirname(STATE_FILE), { recursive: true });
7421
- await fs16.writeFile(STATE_FILE, JSON.stringify(state), "utf8");
7928
+ await fs15.mkdir(path15.dirname(STATE_FILE), { recursive: true });
7929
+ await fs15.writeFile(STATE_FILE, JSON.stringify(state), "utf8");
7422
7930
  }
7423
7931
  function getCurrentVersion() {
7424
7932
  return getPackageVersion();
@@ -7473,6 +7981,7 @@ async function checkForUpdate() {
7473
7981
  // src/tui/commands.ts
7474
7982
  var SLASH_COMMANDS = [
7475
7983
  { name: "auth", aliases: ["/connect", "/login"], description: "Connect your OpenAI account via browser OAuth", category: "auth" },
7984
+ { name: "auth-gemini", aliases: ["/login-gemini", "/gemini"], description: "Connect your Google account via browser OAuth (Gemini)", category: "auth" },
7476
7985
  { name: "auth-codex", aliases: ["/import-codex"], description: "Import auth from existing Codex CLI", category: "auth" },
7477
7986
  { name: "auth-status", aliases: [], description: "Check auth connection status", category: "auth" },
7478
7987
  { name: "logout", aliases: [], description: "Clear stored auth", category: "auth" },
@@ -7579,6 +8088,14 @@ function getUnifiedSuggestions(partial, allSkills) {
7579
8088
  }));
7580
8089
  return [...cmdHits, ...skillHits];
7581
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
+ }
7582
8099
  function extractAtMention(text) {
7583
8100
  const lastAt = text.lastIndexOf("@");
7584
8101
  if (lastAt === -1) return null;
@@ -7603,7 +8120,9 @@ function truncate3(value, max = 96) {
7603
8120
  }
7604
8121
 
7605
8122
  // src/tui/components.tsx
7606
- 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";
7607
8126
 
7608
8127
  // src/tui/layout.ts
7609
8128
  var DEFAULT_TERMINAL_WIDTH = 80;
@@ -7611,11 +8130,19 @@ var MIN_TERMINAL_WIDTH = 20;
7611
8130
  function getTerminalWidth(columns) {
7612
8131
  return Math.max(MIN_TERMINAL_WIDTH, columns ?? process.stdout.columns ?? DEFAULT_TERMINAL_WIDTH);
7613
8132
  }
7614
- function getObservedTerminalWidth(...columns) {
8133
+ function normalizeObservedTerminalWidth(fallbackWidth, ...columns) {
7615
8134
  const observed = columns.filter((value) => typeof value === "number" && Number.isFinite(value) && value > 0);
7616
- if (observed.length === 0) return DEFAULT_TERMINAL_WIDTH;
8135
+ if (observed.length === 0) {
8136
+ return Math.max(MIN_TERMINAL_WIDTH, fallbackWidth);
8137
+ }
7617
8138
  return Math.max(MIN_TERMINAL_WIDTH, Math.min(...observed));
7618
8139
  }
8140
+ function getObservedTerminalWidth(...columns) {
8141
+ return normalizeObservedTerminalWidth(DEFAULT_TERMINAL_WIDTH, ...columns);
8142
+ }
8143
+ function getStableObservedTerminalWidth(currentWidth, ...columns) {
8144
+ return normalizeObservedTerminalWidth(currentWidth, ...columns);
8145
+ }
7619
8146
  function insetWidth(width, inset) {
7620
8147
  return Math.max(1, width - inset);
7621
8148
  }
@@ -7731,7 +8258,7 @@ function renderInline(text, codeColor = source_default.cyan, linkColor = source_
7731
8258
  }
7732
8259
 
7733
8260
  // src/tui/components.tsx
7734
- import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
8261
+ import { Fragment, jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
7735
8262
  var GUTTER = {
7736
8263
  user: "\u203A",
7737
8264
  agent: "\u25AA",
@@ -7755,7 +8282,12 @@ function borderedContentWidth(width) {
7755
8282
  function wrapText(value, width) {
7756
8283
  return wrapAnsi(value, Math.max(1, width), { trim: false, hard: true });
7757
8284
  }
7758
- 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 }) {
7759
8291
  const theme = useTheme();
7760
8292
  const contentWidth = resolveWidth(width);
7761
8293
  const bodyWidth = indentedWidth(contentWidth);
@@ -7766,12 +8298,18 @@ function UserMessage({ text, width }) {
7766
8298
  GUTTER.user,
7767
8299
  " "
7768
8300
  ] }),
7769
- /* @__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
+ ] })
7770
8308
  ] }),
7771
8309
  /* @__PURE__ */ jsx4(Box4, { marginLeft: 2, width: bodyWidth, children: /* @__PURE__ */ jsx4(Text4, { color: theme.text, wrap: "wrap", children: wrappedText }) })
7772
8310
  ] });
7773
- }
7774
- function AgentMessage({ text, width }) {
8311
+ });
8312
+ var AgentMessage = memo(function AgentMessage2({ text, width }) {
7775
8313
  const contentWidth = resolveWidth(width);
7776
8314
  const bodyWidth = indentedWidth(contentWidth);
7777
8315
  const theme = useTheme();
@@ -7787,6 +8325,20 @@ function AgentMessage({ text, width }) {
7787
8325
  ] }),
7788
8326
  /* @__PURE__ */ jsx4(Box4, { marginLeft: 2, width: bodyWidth, children: /* @__PURE__ */ jsx4(Text4, { wrap: "wrap", children: wrappedText }) })
7789
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
+ ] });
7790
8342
  }
7791
8343
  function TaskPanel({
7792
8344
  tasks: tasks2,
@@ -7819,7 +8371,7 @@ function TaskPanel({
7819
8371
  ] })
7820
8372
  ] });
7821
8373
  }
7822
- function ToolActivitySummary({
8374
+ var ToolActivitySummary = memo(function ToolActivitySummary2({
7823
8375
  summary,
7824
8376
  tools,
7825
8377
  expanded = false,
@@ -7827,43 +8379,50 @@ function ToolActivitySummary({
7827
8379
  }) {
7828
8380
  const theme = useTheme();
7829
8381
  const contentWidth = indentedWidth(resolveWidth(width));
8382
+ const innerWidth = indentedWidth(contentWidth, 4);
7830
8383
  if (expanded) {
7831
- return /* @__PURE__ */ jsxs3(Box4, { flexDirection: "column", marginLeft: 2, marginBottom: 0, width: contentWidth, children: [
7832
- /* @__PURE__ */ jsxs3(Text4, { color: theme.muted, dimColor: true, wrap: "wrap", children: [
7833
- GUTTER.tool,
7834
- " ",
7835
- summary
7836
- ] }),
7837
- tools.map((t, i) => {
7838
- const dur = t.durationMs ? ` (${(t.durationMs / 1e3).toFixed(1)}s)` : "";
7839
- const prefix = i === tools.length - 1 ? "\u2514" : "\u251C";
7840
- return /* @__PURE__ */ jsxs3(Text4, { color: theme.muted, dimColor: true, wrap: "wrap", children: [
7841
- " ",
7842
- prefix,
7843
- " ",
7844
- GUTTER.success,
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,
7845
8389
  " ",
7846
- t.description,
7847
- dur
7848
- ] }, i);
7849
- })
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
+ ] })
7850
8406
  ] });
7851
8407
  }
7852
8408
  const lastTarget = tools.length > 0 ? tools[tools.length - 1].description : "";
7853
8409
  const hint = tools.length > 1 ? " (ctrl+o to expand)" : "";
7854
- return /* @__PURE__ */ jsxs3(Box4, { flexDirection: "column", marginLeft: 2, marginBottom: 0, width: contentWidth, children: [
7855
- /* @__PURE__ */ jsxs3(Text4, { color: theme.muted, dimColor: true, wrap: "wrap", children: [
7856
- GUTTER.tool,
7857
- " ",
7858
- summary,
7859
- hint
7860
- ] }),
7861
- lastTarget && tools.length > 1 && /* @__PURE__ */ jsxs3(Text4, { color: theme.muted, dimColor: true, wrap: "wrap", children: [
7862
- " \u2514 ",
7863
- 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
+ ] })
7864
8423
  ] })
7865
8424
  ] });
7866
- }
8425
+ });
7867
8426
  function SubAgentIndicator({
7868
8427
  agentType,
7869
8428
  goal,
@@ -7912,23 +8471,30 @@ function SubAgentIndicator({
7912
8471
  }
7913
8472
  );
7914
8473
  }
7915
- function SystemMessage({ text, width }) {
8474
+ var SystemMessage = memo(function SystemMessage2({ text, width }) {
7916
8475
  const theme = useTheme();
7917
8476
  const contentWidth = resolveWidth(width);
7918
8477
  const indentedContentWidth = indentedWidth(contentWidth);
7919
8478
  const wrappedIndentedText = wrapText(text, indentedContentWidth);
7920
8479
  const wrappedText = wrapText(text, contentWidth);
7921
- if (text.trimStart().startsWith("\u2713") || text.trimStart().startsWith("\u2717")) {
8480
+ const trimmed = text.trimStart();
8481
+ if (trimmed.startsWith("\u2713") || trimmed.startsWith("\u2717")) {
7922
8482
  return /* @__PURE__ */ jsx4(Box4, { marginLeft: 2, width: indentedContentWidth, children: /* @__PURE__ */ jsx4(Text4, { color: theme.muted, dimColor: true, wrap: "wrap", children: wrappedIndentedText }) });
7923
8483
  }
7924
- 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")) {
7925
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) }) });
7926
8492
  }
7927
- if (text.trimStart().startsWith(">")) {
8493
+ if (trimmed.startsWith(">")) {
7928
8494
  return /* @__PURE__ */ jsx4(Box4, { width: contentWidth, children: /* @__PURE__ */ jsx4(Text4, { color: theme.muted, dimColor: true, wrap: "wrap", children: wrappedText }) });
7929
8495
  }
7930
8496
  return /* @__PURE__ */ jsx4(Box4, { width: contentWidth, children: /* @__PURE__ */ jsx4(Text4, { color: theme.muted, wrap: "wrap", children: wrapText(`${GUTTER.system} ${text.trim()}`, contentWidth) }) });
7931
- }
8497
+ });
7932
8498
  function PromptPrefix({
7933
8499
  busy,
7934
8500
  frame,
@@ -7947,25 +8513,131 @@ function PromptPrefix({
7947
8513
  " "
7948
8514
  ] });
7949
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
+ }
7950
8540
  function PendingUpdateCard({
7951
8541
  count,
7952
8542
  summary,
8543
+ fileName,
8544
+ updateType,
8545
+ oldContent,
8546
+ newContent,
8547
+ active,
8548
+ onAccept,
8549
+ onReject,
8550
+ onFeedback,
7953
8551
  width
7954
8552
  }) {
7955
8553
  const theme = useTheme();
7956
8554
  const contentWidth = resolveWidth(width);
7957
- 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);
7958
8630
  return /* @__PURE__ */ jsxs3(
7959
8631
  Box4,
7960
8632
  {
7961
8633
  borderStyle: "single",
7962
- borderColor: theme.pending,
8634
+ borderColor: theme.warning,
7963
8635
  paddingX: 1,
7964
- marginBottom: 1,
8636
+ marginBottom: 0,
7965
8637
  flexDirection: "column",
7966
8638
  width: contentWidth,
7967
8639
  children: [
7968
- /* @__PURE__ */ jsxs3(Box4, { width: borderedContentWidth(contentWidth), children: [
8640
+ /* @__PURE__ */ jsxs3(Box4, { width: innerWidth, children: [
7969
8641
  /* @__PURE__ */ jsxs3(Text4, { color: theme.pending, bold: true, children: [
7970
8642
  GUTTER.pending,
7971
8643
  " "
@@ -7977,13 +8649,70 @@ function PendingUpdateCard({
7977
8649
  " awaiting review"
7978
8650
  ] })
7979
8651
  ] }),
7980
- /* @__PURE__ */ jsx4(Box4, { marginLeft: 2, width: bodyWidth, children: /* @__PURE__ */ jsx4(Text4, { color: theme.muted, wrap: "wrap", children: summary }) }),
7981
- /* @__PURE__ */ jsx4(Box4, { marginLeft: 2, marginTop: 0, width: bodyWidth, children: /* @__PURE__ */ jsxs3(Text4, { color: theme.muted, dimColor: true, wrap: "wrap", children: [
7982
- /* @__PURE__ */ jsx4(Text4, { bold: true, color: theme.secondary, children: "a" }),
7983
- " accept ",
7984
- /* @__PURE__ */ jsx4(Text4, { bold: true, color: theme.error, children: "r" }),
7985
- " reject"
7986
- ] }) })
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..." }) })
7987
8716
  ]
7988
8717
  }
7989
8718
  );
@@ -7991,12 +8720,67 @@ function PendingUpdateCard({
7991
8720
  function QuestionCard({
7992
8721
  question,
7993
8722
  options,
8723
+ active,
8724
+ onSelect,
7994
8725
  width
7995
8726
  }) {
7996
8727
  const theme = useTheme();
7997
8728
  const contentWidth = resolveWidth(width);
7998
8729
  const innerWidth = borderedContentWidth(contentWidth);
7999
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;
8000
8784
  return /* @__PURE__ */ jsxs3(
8001
8785
  Box4,
8002
8786
  {
@@ -8015,18 +8799,38 @@ function QuestionCard({
8015
8799
  /* @__PURE__ */ jsx4(Text4, { bold: true, color: theme.warning, children: "Agent needs your input" })
8016
8800
  ] }),
8017
8801
  /* @__PURE__ */ jsx4(Box4, { marginLeft: 2, marginTop: 0, width: bodyWidth, children: /* @__PURE__ */ jsx4(Text4, { color: theme.text, wrap: "wrap", children: question }) }),
8018
- 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: [
8019
- /* @__PURE__ */ jsx4(Text4, { color: theme.accent, bold: true, children: idx + 1 }),
8020
- /* @__PURE__ */ jsxs3(Text4, { color: theme.text, wrap: "wrap", children: [
8021
- " ",
8022
- opt.label
8023
- ] }),
8024
- /* @__PURE__ */ jsxs3(Text4, { color: theme.muted, dimColor: true, wrap: "wrap", children: [
8025
- " \u2014 ",
8026
- opt.description
8027
- ] })
8028
- ] }, opt.label)) }),
8029
- /* @__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..." }) })
8030
8834
  ]
8031
8835
  }
8032
8836
  );
@@ -8221,22 +9025,22 @@ function FooterBar({
8221
9025
  var STREAM_FLUSH_PATTERN = /[.!?]\s|\n|^#{1,3}\s|^[-*]\s/m;
8222
9026
  var STREAM_FLUSH_INTERVAL_MS = 80;
8223
9027
  function splitMessagesForRender(messages, busy) {
8224
- if (!busy || messages.length === 0) {
9028
+ if (messages.length === 0) {
8225
9029
  return {
8226
9030
  staticMessages: messages,
8227
9031
  dynamicMessages: []
8228
9032
  };
8229
9033
  }
8230
9034
  const last = messages[messages.length - 1];
8231
- if (!last || last.role !== "assistant") {
9035
+ if (last && (last.role === "assistant" ? busy : !busy && last.role === "system")) {
8232
9036
  return {
8233
- staticMessages: messages,
8234
- dynamicMessages: []
9037
+ staticMessages: messages.slice(0, -1),
9038
+ dynamicMessages: [last]
8235
9039
  };
8236
9040
  }
8237
9041
  return {
8238
- staticMessages: messages.slice(0, -1),
8239
- dynamicMessages: [last]
9042
+ staticMessages: messages,
9043
+ dynamicMessages: []
8240
9044
  };
8241
9045
  }
8242
9046
  function createSentenceStreamBuffer({
@@ -8285,10 +9089,10 @@ function createSentenceStreamBuffer({
8285
9089
  }
8286
9090
 
8287
9091
  // src/tui/hooks/use-animated-frame.ts
8288
- import { useState as useState4, useEffect as useEffect2 } from "react";
9092
+ import { useState as useState5, useEffect as useEffect2 } from "react";
8289
9093
  var SPINNER_FRAMES = ["\u25D0", "\u25D3", "\u25D1", "\u25D2"];
8290
9094
  function useAnimatedFrame(active) {
8291
- const [index, setIndex] = useState4(0);
9095
+ const [index, setIndex] = useState5(0);
8292
9096
  useEffect2(() => {
8293
9097
  if (!active) {
8294
9098
  setIndex(0);
@@ -8301,28 +9105,49 @@ function useAnimatedFrame(active) {
8301
9105
  }
8302
9106
 
8303
9107
  // src/tui/hooks/use-terminal-width.ts
8304
- import { useState as useState5, useEffect as useEffect3 } from "react";
9108
+ import { useState as useState6, useEffect as useEffect3 } from "react";
8305
9109
  import { useStdout as useStdout2 } from "ink";
9110
+ var RESIZE_DEBOUNCE_MS = 50;
8306
9111
  function useTerminalWidth() {
8307
9112
  const { stdout } = useStdout2();
8308
- const [terminalWidth, setTerminalWidth] = useState5(
9113
+ const [terminalWidth, setTerminalWidth] = useState6(
8309
9114
  () => getObservedTerminalWidth(stdout.columns, process.stdout.columns)
8310
9115
  );
8311
9116
  useEffect3(() => {
8312
9117
  const stream = stdout;
8313
- const updateWidth = () => {
8314
- const nextWidth = getObservedTerminalWidth(stream.columns, process.stdout.columns);
8315
- 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
+ });
8316
9124
  };
8317
- 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();
8318
9135
  if (typeof stream.on === "function") {
8319
- stream.on("resize", updateWidth);
9136
+ stream.on("resize", scheduleWidthUpdate);
8320
9137
  return () => {
9138
+ if (resizeTimer) {
9139
+ clearTimeout(resizeTimer);
9140
+ }
8321
9141
  if (typeof stream.off === "function") {
8322
- stream.off("resize", updateWidth);
9142
+ stream.off("resize", scheduleWidthUpdate);
8323
9143
  }
8324
9144
  };
8325
9145
  }
9146
+ return () => {
9147
+ if (resizeTimer) {
9148
+ clearTimeout(resizeTimer);
9149
+ }
9150
+ };
8326
9151
  }, [stdout]);
8327
9152
  return terminalWidth;
8328
9153
  }
@@ -8331,7 +9156,7 @@ function useTerminalWidth() {
8331
9156
  import { startTransition } from "react";
8332
9157
 
8333
9158
  // src/lib/workspace/init-agents-md.ts
8334
- import fs17 from "fs/promises";
9159
+ import fs16 from "fs/promises";
8335
9160
  import path16 from "path";
8336
9161
  var IGNORED_DIRS2 = /* @__PURE__ */ new Set([
8337
9162
  "node_modules",
@@ -8371,7 +9196,7 @@ async function scanDirectoryShallow(dir, maxDepth = 2, depth = 0) {
8371
9196
  const results = [];
8372
9197
  if (depth > maxDepth) return results;
8373
9198
  try {
8374
- const entries = await fs17.readdir(dir, { withFileTypes: true });
9199
+ const entries = await fs16.readdir(dir, { withFileTypes: true });
8375
9200
  for (const entry of entries) {
8376
9201
  if (IGNORED_DIRS2.has(entry.name)) continue;
8377
9202
  if (entry.name.startsWith(".") && depth === 0 && entry.isDirectory()) continue;
@@ -8382,7 +9207,7 @@ async function scanDirectoryShallow(dir, maxDepth = 2, depth = 0) {
8382
9207
  const children = await scanDirectoryShallow(fullPath, maxDepth, depth + 1);
8383
9208
  results.push(...children);
8384
9209
  } else {
8385
- const stat = await fs17.stat(fullPath).catch(() => null);
9210
+ const stat = await fs16.stat(fullPath).catch(() => null);
8386
9211
  results.push({ path: relativePath, size: stat?.size ?? 0, isDir: false });
8387
9212
  }
8388
9213
  }
@@ -8394,7 +9219,7 @@ async function readKeyFiles(dir) {
8394
9219
  const contents = {};
8395
9220
  for (const name of INTERESTING_FILES) {
8396
9221
  try {
8397
- const content = await fs17.readFile(path16.join(dir, name), "utf8");
9222
+ const content = await fs16.readFile(path16.join(dir, name), "utf8");
8398
9223
  contents[name] = content.slice(0, 2e3);
8399
9224
  } catch {
8400
9225
  }
@@ -8480,7 +9305,7 @@ ${scanData.slice(0, 25e3)}`;
8480
9305
 
8481
9306
  // src/lib/preview/server.ts
8482
9307
  import http2 from "http";
8483
- import fs18 from "fs";
9308
+ import fs17 from "fs";
8484
9309
  import path17 from "path";
8485
9310
 
8486
9311
  // src/lib/preview/latex-to-html.ts
@@ -8752,7 +9577,7 @@ function startPreviewServer(texPath) {
8752
9577
  let currentHash = "";
8753
9578
  function getContentHash() {
8754
9579
  try {
8755
- const content = fs18.readFileSync(resolved, "utf8");
9580
+ const content = fs17.readFileSync(resolved, "utf8");
8756
9581
  return `${content.length}-${content.slice(0, 100)}-${content.slice(-100)}`;
8757
9582
  } catch {
8758
9583
  return "error";
@@ -8760,7 +9585,7 @@ function startPreviewServer(texPath) {
8760
9585
  }
8761
9586
  function renderPage() {
8762
9587
  try {
8763
- const latex = fs18.readFileSync(resolved, "utf8");
9588
+ const latex = fs17.readFileSync(resolved, "utf8");
8764
9589
  const htmlContent = latexToHtml(latex);
8765
9590
  currentHash = getContentHash();
8766
9591
  return HTML_TEMPLATE.replace("{{CONTENT}}", htmlContent);
@@ -8807,7 +9632,7 @@ async function executeSlashCommand(cmd, args, ctx) {
8807
9632
  history,
8808
9633
  skills: skills2,
8809
9634
  workspaceFiles,
8810
- sessionId,
9635
+ sessionId: sessionId2,
8811
9636
  sessionTokens,
8812
9637
  agentMode,
8813
9638
  addSystemMessage,
@@ -8846,6 +9671,23 @@ async function executeSlashCommand(cmd, args, ctx) {
8846
9671
  }
8847
9672
  break;
8848
9673
  }
9674
+ case "auth-gemini": {
9675
+ addSystemMessage("Opening browser for Google login...");
9676
+ addSystemMessage("Note: Uses Gemini CLI credentials. Google may restrict third-party use.");
9677
+ setBusy(true);
9678
+ try {
9679
+ const { loginWithGemini } = await import("./gemini-login-EYY3EFH4.js");
9680
+ const result = await loginWithGemini({ homeDir });
9681
+ setAuthStatus("connected");
9682
+ addSystemMessage(`Connected Google account ${result.tokens.email}`);
9683
+ addSystemMessage("Switch to Gemini: /config provider gemini");
9684
+ } catch (err) {
9685
+ addSystemMessage(`Auth failed: ${err instanceof Error ? err.message : String(err)}`);
9686
+ } finally {
9687
+ setBusy(false);
9688
+ }
9689
+ break;
9690
+ }
8849
9691
  case "auth-codex": {
8850
9692
  addSystemMessage("Importing Codex CLI auth...");
8851
9693
  setBusy(true);
@@ -8934,7 +9776,7 @@ async function executeSlashCommand(cmd, args, ctx) {
8934
9776
  addSystemMessage("No workspace. Run /init first.");
8935
9777
  break;
8936
9778
  }
8937
- const { listSessions: listSessions2 } = await import("./sessions-GRES2MUV.js");
9779
+ const { listSessions: listSessions2 } = await import("./sessions-KL4LUGD7.js");
8938
9780
  const foundSessions = await listSessions2(workspacePath);
8939
9781
  if (foundSessions.length === 0) {
8940
9782
  addSystemMessage("No previous sessions found.");
@@ -9484,22 +10326,36 @@ function parseCharterYaml(raw) {
9484
10326
 
9485
10327
  // src/tui/helpers/render-message.tsx
9486
10328
  import { jsx as jsx5 } from "react/jsx-runtime";
9487
- function renderConversationMessage(message, key, expanded = false, width) {
9488
- if (message.role === "system") {
9489
- if (message.text.startsWith("__tool_summary__")) {
9490
- try {
9491
- const data = JSON.parse(message.text.slice("__tool_summary__".length));
9492
- return /* @__PURE__ */ jsx5(ToolActivitySummary, { summary: data.summary, tools: data.tools, expanded, width }, key);
9493
- } catch {
9494
- 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));
9495
10353
  }
10354
+ } else {
10355
+ elements.push(/* @__PURE__ */ jsx5(AgentMessage, { text: message.text, width }, key));
9496
10356
  }
9497
- return /* @__PURE__ */ jsx5(SystemMessage, { text: message.text, width }, key);
9498
10357
  }
9499
- if (message.role === "user") {
9500
- return /* @__PURE__ */ jsx5(UserMessage, { text: message.text, width }, key);
9501
- }
9502
- return /* @__PURE__ */ jsx5(AgentMessage, { text: message.text, width }, key);
10358
+ return elements;
9503
10359
  }
9504
10360
 
9505
10361
  // src/tui/app.tsx
@@ -9510,55 +10366,84 @@ function App({
9510
10366
  }) {
9511
10367
  const app = useApp();
9512
10368
  const abortRef = useRef2(null);
9513
- const [input2, setInput] = useState6("");
9514
- const [composerFocused, setComposerFocused] = useState6(true);
9515
- const [busy, setBusy] = useState6(false);
9516
- const [authStatus, setAuthStatus] = useState6(initialState.authStatus);
9517
- const [workspacePath, setWorkspacePath] = useState6(initialState.workspacePath);
9518
- const [workspaceFiles, setWorkspaceFiles] = useState6([]);
9519
- const [skills2, setSkills] = useState6([]);
9520
- const [messages, setMessages] = useState6([]);
9521
- const [messageRenderVersion, setMessageRenderVersion] = useState6(0);
9522
- const [history, setHistory] = useState6([]);
9523
- const [activeSkills, setActiveSkills] = useState6([]);
9524
- const [pendingUpdates, setPendingUpdates] = useState6(initialState.pendingUpdates);
9525
- const [statusLine, setStatusLine] = useState6("");
9526
- const [currentToolActivity, setCurrentToolActivity] = useState6("");
9527
- const [turnToolCount, setTurnToolCount] = useState6(0);
9528
- const [subAgentProgress, setSubAgentProgress] = useState6(null);
9529
- const [toolActivityExpanded, setToolActivityExpanded] = useState6(false);
9530
- const [taskPanelVisible, setTaskPanelVisible] = useState6(true);
9531
- 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);
9532
10387
  const turnToolLogRef = useRef2([]);
9533
- const [sessionTokens, setSessionTokens] = useState6(() => createSessionUsage());
9534
- const [tokenDisplay, setTokenDisplay] = useState6("");
9535
- const [showSuggestions, setShowSuggestions] = useState6(false);
9536
- const [agentMode, setAgentMode] = useState6("manual-review");
9537
- 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({
9538
10393
  status: "idle",
9539
10394
  planningHistory: []
9540
10395
  });
9541
- const [theme, setTheme] = useState6("dark");
9542
- const [config, setConfig] = useState6(null);
9543
- const [cursorToEnd, setCursorToEnd] = useState6(0);
9544
- const [screen, setScreen] = useState6("main");
9545
- const [resumeSessions, setResumeSessions] = useState6([]);
9546
- const [ctrlCPending, setCtrlCPending] = useState6(false);
9547
- const sessionId = useMemo3(() => crypto.randomUUID(), []);
9548
- 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(), []);
9549
10404
  const deferredPendingUpdates = useDeferredValue(pendingUpdates);
10405
+ const visiblePendingUpdates = deferredPendingUpdates.length > 0 ? deferredPendingUpdates : pendingUpdates;
9550
10406
  const activityFrame = useAnimatedFrame(busy);
9551
10407
  const terminalWidth = useTerminalWidth();
9552
10408
  const contentWidth = insetWidth(terminalWidth, 2);
9553
10409
  const panelInnerWidth = insetWidth(contentWidth, 4);
9554
10410
  const panelBodyWidth = insetWidth(panelInnerWidth, 2);
9555
- const [agentQuestion, setAgentQuestion] = useState6(null);
10411
+ const [agentQuestion, setAgentQuestion] = useState7(null);
9556
10412
  const previewRef = useRef2(null);
9557
10413
  const ctrlCTimerRef = useRef2(null);
9558
- const isHome = deferredMessages.length === 0 && !busy;
9559
- const { staticMessages, dynamicMessages } = useMemo3(
9560
- () => splitMessagesForRender(deferredMessages, busy),
9561
- [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]
9562
10447
  );
9563
10448
  const hasWorkspace = workspacePath !== null;
9564
10449
  const hasAuth = authStatus === "connected";
@@ -9571,7 +10456,7 @@ function App({
9571
10456
  const pending = getPendingQuestion();
9572
10457
  if (pending && (!agentQuestion || pending.question.id !== agentQuestion.question.id)) {
9573
10458
  setAgentQuestion(pending);
9574
- setComposerFocused(true);
10459
+ setComposerFocused(false);
9575
10460
  }
9576
10461
  }, 200);
9577
10462
  return () => clearInterval(interval);
@@ -9626,15 +10511,16 @@ function App({
9626
10511
  }
9627
10512
  };
9628
10513
  }, []);
9629
- const [selectedSuggestion, setSelectedSuggestion] = useState6(-1);
9630
- const atMention = useMemo3(() => extractAtMention(input2), [input2]);
9631
- 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(() => {
9632
10518
  if (atMention) {
9633
10519
  return getFileSuggestions(atMention.partial, workspaceFiles);
9634
10520
  }
9635
- if (!input2.startsWith("/")) return [];
9636
- return getUnifiedSuggestions(input2, skills2);
9637
- }, [input2, skills2, atMention, workspaceFiles]);
10521
+ if (!slashTrigger) return [];
10522
+ return getUnifiedSuggestions(`/${slashTrigger.partial}`, skills2);
10523
+ }, [input2, skills2, atMention, slashTrigger, workspaceFiles]);
9638
10524
  useEffect4(() => {
9639
10525
  setSelectedSuggestion(-1);
9640
10526
  }, [suggestions.length, input2]);
@@ -9643,8 +10529,9 @@ function App({
9643
10529
  if (s.kind === "file" && atMention) {
9644
10530
  const before = input2.slice(0, atMention.start);
9645
10531
  setInput(`${before}@${s.path} `);
9646
- } else if (s.kind === "command" || s.kind === "skill") {
9647
- 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}`);
9648
10535
  }
9649
10536
  setSelectedSuggestion(-1);
9650
10537
  setCursorToEnd((c) => c + 1);
@@ -9701,10 +10588,8 @@ function App({
9701
10588
  });
9702
10589
  }
9703
10590
  function replaceMessages(nextMessages) {
9704
- startTransition2(() => {
9705
- setMessageRenderVersion((current) => current + 1);
9706
- setMessages(nextMessages);
9707
- });
10591
+ setMessages(nextMessages);
10592
+ setMessageRenderVersion((current) => current + 1);
9708
10593
  }
9709
10594
  const slashCtx = {
9710
10595
  homeDir,
@@ -9715,7 +10600,7 @@ function App({
9715
10600
  history,
9716
10601
  skills: skills2,
9717
10602
  workspaceFiles,
9718
- sessionId,
10603
+ sessionId: sessionId2,
9719
10604
  sessionTokens,
9720
10605
  agentMode,
9721
10606
  previewRef,
@@ -9743,7 +10628,7 @@ function App({
9743
10628
  if (pendingUpdates.length === 0 || !workspacePath) return;
9744
10629
  const [next, ...rest] = pendingUpdates;
9745
10630
  await applyProposedUpdate(workspacePath, next);
9746
- await appendSessionEvent(workspacePath, sessionId, {
10631
+ await appendSessionEvent(workspacePath, sessionId2, {
9747
10632
  type: "update.accepted",
9748
10633
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
9749
10634
  payload: { key: next.key, summary: next.summary }
@@ -9758,7 +10643,7 @@ function App({
9758
10643
  function rejectNextPendingUpdate() {
9759
10644
  if (pendingUpdates.length === 0 || !workspacePath) return;
9760
10645
  const [next, ...rest] = pendingUpdates;
9761
- void appendSessionEvent(workspacePath, sessionId, {
10646
+ void appendSessionEvent(workspacePath, sessionId2, {
9762
10647
  type: "update.rejected",
9763
10648
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
9764
10649
  payload: { key: next.key, summary: next.summary }
@@ -9768,6 +10653,19 @@ function App({
9768
10653
  addSystemMessage(`Rejected: ${next.summary}`);
9769
10654
  });
9770
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
+ }
9771
10669
  function clearCtrlCPending() {
9772
10670
  if (ctrlCTimerRef.current) {
9773
10671
  clearTimeout(ctrlCTimerRef.current);
@@ -9822,7 +10720,7 @@ function App({
9822
10720
  summary: `Research charter: ${charter.researchQuestion}`
9823
10721
  };
9824
10722
  await applyProposedUpdate(workspacePath, charterUpdate);
9825
- await appendSessionEvent(workspacePath, sessionId, {
10723
+ await appendSessionEvent(workspacePath, sessionId2, {
9826
10724
  type: "charter.approved",
9827
10725
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
9828
10726
  payload: { charterId: charter.id }
@@ -9835,7 +10733,7 @@ function App({
9835
10733
  }
9836
10734
  function rejectCharter() {
9837
10735
  if (!planningState.charter || !workspacePath) return;
9838
- void appendSessionEvent(workspacePath, sessionId, {
10736
+ void appendSessionEvent(workspacePath, sessionId2, {
9839
10737
  type: "charter.rejected",
9840
10738
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
9841
10739
  payload: { charterId: planningState.charter.id }
@@ -9843,7 +10741,7 @@ function App({
9843
10741
  setPlanningState({ status: "idle", planningHistory: [] });
9844
10742
  addSystemMessage("Charter cancelled. Planning reset.");
9845
10743
  }
9846
- useInput4((key, inputKey) => {
10744
+ useInput5((key, inputKey) => {
9847
10745
  if (inputKey.ctrl && key === "c") {
9848
10746
  if (busy) {
9849
10747
  clearCtrlCPending();
@@ -9916,14 +10814,7 @@ function App({
9916
10814
  return;
9917
10815
  }
9918
10816
  }
9919
- if (key === "a" && pendingUpdates.length > 0) {
9920
- void acceptNextPendingUpdate();
9921
- return;
9922
- }
9923
- if (key === "r" && pendingUpdates.length > 0) {
9924
- rejectNextPendingUpdate();
9925
- return;
9926
- }
10817
+ if (pendingUpdates.length > 0 && !agentQuestion || agentQuestion) return;
9927
10818
  if (key === "i" || key.length === 1 && !inputKey.ctrl && !inputKey.meta && !inputKey.tab) {
9928
10819
  setComposerFocused(true);
9929
10820
  if (key !== "i") setInput((c) => c + key);
@@ -9935,24 +10826,9 @@ function App({
9935
10826
  if (handleDropdownSelect()) return;
9936
10827
  const trimmed = value.trim();
9937
10828
  if (!trimmed) return;
9938
- if (agentQuestion) {
9939
- const options = agentQuestion.question.options;
9940
- const numChoice = parseInt(trimmed, 10);
9941
- if (!isNaN(numChoice) && numChoice >= 1 && numChoice <= options.length) {
9942
- const picked = options[numChoice - 1];
9943
- addSystemMessage(`> ${picked.label}`);
9944
- agentQuestion.resolve({ questionId: agentQuestion.question.id, answer: picked.label, isCustom: false });
9945
- } else {
9946
- addSystemMessage(`> ${trimmed}`);
9947
- agentQuestion.resolve({ questionId: agentQuestion.question.id, answer: trimmed, isCustom: true });
9948
- }
9949
- clearPendingQuestion();
9950
- setAgentQuestion(null);
9951
- setInput("");
9952
- return;
9953
- }
10829
+ if (agentQuestion) return;
9954
10830
  if (busy) return;
9955
- if (trimmed.startsWith("/") && !trimmed.includes(" ") && applyAutocomplete()) {
10831
+ if (dropdownVisible && applyAutocomplete()) {
9956
10832
  return;
9957
10833
  }
9958
10834
  setInput("");
@@ -9995,9 +10871,11 @@ function App({
9995
10871
  if (!workspacePath) return;
9996
10872
  turnToolLogRef.current = [];
9997
10873
  setTurnToolCount(0);
9998
- setSubAgentProgress(null);
10874
+ setActiveToolActivities({});
10875
+ setSubAgentProgress({});
9999
10876
  const controller = new AbortController();
10000
10877
  let streamBuffer = null;
10878
+ let focusPendingReviewOnComplete = false;
10001
10879
  abortRef.current = controller;
10002
10880
  setBusy(true);
10003
10881
  startTransition2(() => {
@@ -10008,7 +10886,7 @@ function App({
10008
10886
  const workspace = await scanWorkspace(workspacePath);
10009
10887
  const workspaceContext = {
10010
10888
  workspaceDir: workspacePath,
10011
- runId: sessionId,
10889
+ runId: sessionId2,
10012
10890
  workspaceFiles: Object.fromEntries(workspace.files.map((f) => [f.key, f.content])),
10013
10891
  availableKeys: workspace.files.map((f) => f.key),
10014
10892
  fileLabels: Object.fromEntries(workspace.files.map((f) => [f.key, f.label]))
@@ -10037,9 +10915,16 @@ function App({
10037
10915
  onToolActivity: (activity) => {
10038
10916
  streamBuffer?.flush();
10039
10917
  if (activity.type === "tool_start") {
10040
- setCurrentToolActivity(activity.description ?? activity.name);
10918
+ setActiveToolActivities((current) => ({
10919
+ ...current,
10920
+ [activity.toolCallId]: activity.description ?? activity.name
10921
+ }));
10041
10922
  } else {
10042
- setCurrentToolActivity("");
10923
+ setActiveToolActivities((current) => {
10924
+ const next = { ...current };
10925
+ delete next[activity.toolCallId];
10926
+ return next;
10927
+ });
10043
10928
  turnToolLogRef.current.push({
10044
10929
  name: activity.name,
10045
10930
  description: activity.description ?? activity.name,
@@ -10053,9 +10938,16 @@ function App({
10053
10938
  },
10054
10939
  onSubAgentProgress: (progress) => {
10055
10940
  if (progress.status === "done") {
10056
- setSubAgentProgress(null);
10941
+ setSubAgentProgress((current) => {
10942
+ const next = { ...current };
10943
+ delete next[progress.agentId];
10944
+ return next;
10945
+ });
10057
10946
  } else {
10058
- setSubAgentProgress({ ...progress });
10947
+ setSubAgentProgress((current) => ({
10948
+ ...current,
10949
+ [progress.agentId]: { ...progress }
10950
+ }));
10059
10951
  }
10060
10952
  },
10061
10953
  onMemoryExtracted: (mems) => {
@@ -10102,9 +10994,10 @@ function App({
10102
10994
  }
10103
10995
  }
10104
10996
  if (reviewRequired.length > 0) {
10105
- startTransition2(() => setPendingUpdates((c) => [...c, ...reviewRequired]));
10997
+ focusPendingReviewOnComplete = true;
10998
+ setPendingUpdates((current) => [...current, ...reviewRequired]);
10106
10999
  }
10107
- await appendSessionEvent(workspacePath, sessionId, {
11000
+ await appendSessionEvent(workspacePath, sessionId2, {
10108
11001
  type: "chat.turn",
10109
11002
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
10110
11003
  payload: {
@@ -10134,8 +11027,10 @@ ${error.stack}` : String(error)}` }
10134
11027
  } finally {
10135
11028
  streamBuffer?.dispose();
10136
11029
  abortRef.current = null;
11030
+ setActiveToolActivities({});
11031
+ setSubAgentProgress({});
10137
11032
  setBusy(false);
10138
- setComposerFocused(true);
11033
+ setComposerFocused(!focusPendingReviewOnComplete);
10139
11034
  if (controller.signal.aborted) {
10140
11035
  addSystemMessage("Agent interrupted.");
10141
11036
  }
@@ -10162,7 +11057,7 @@ ${error.stack}` : String(error)}` }
10162
11057
  const workspace = await scanWorkspace(workspacePath);
10163
11058
  const workspaceContext = {
10164
11059
  workspaceDir: workspacePath,
10165
- runId: sessionId,
11060
+ runId: sessionId2,
10166
11061
  workspaceFiles: Object.fromEntries(workspace.files.map((f) => [f.key, f.content])),
10167
11062
  availableKeys: workspace.files.map((f) => f.key),
10168
11063
  fileLabels: Object.fromEntries(workspace.files.map((f) => [f.key, f.label]))
@@ -10202,7 +11097,7 @@ ${error.stack}` : String(error)}` }
10202
11097
  const charter = parseCharterYaml(result.detectedCharter);
10203
11098
  setPlanningState((prev) => ({ ...prev, status: "charter-review", charter }));
10204
11099
  if (workspacePath) {
10205
- await appendSessionEvent(workspacePath, sessionId, {
11100
+ await appendSessionEvent(workspacePath, sessionId2, {
10206
11101
  type: "charter.generated",
10207
11102
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
10208
11103
  payload: { charterId: charter.id, researchQuestion: charter.researchQuestion }
@@ -10242,10 +11137,10 @@ ${error.stack}` : String(error)}` }
10242
11137
  else statusParts.push("no workspace");
10243
11138
  if (skills2.length > 0) statusParts.push(`${skills2.length} skills`);
10244
11139
  statusParts.push(agentMode);
10245
- if (deferredPendingUpdates.length > 0) statusParts.push(`${deferredPendingUpdates.length} pending`);
11140
+ if (pendingUpdates.length > 0) statusParts.push(`${pendingUpdates.length} pending`);
10246
11141
  const themeColors = getThemeColors(theme);
10247
- const statusColor = busy ? themeColors.warning : ctrlCPending ? themeColors.warning : !hasAuth ? themeColors.error : deferredPendingUpdates.length > 0 ? themeColors.pending : themeColors.secondary;
10248
- 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(() => [
10249
11144
  { key: "defaults.model", label: "Model", values: [...getAvailableModels()], current: config?.defaults.model ?? "gpt-5.4" },
10250
11145
  { key: "theme", label: "Theme", values: [...themeValues], current: theme },
10251
11146
  { key: "defaults.reasoningEffort", label: "Reasoning effort", values: ["low", "medium", "high", "xhigh"], current: config?.defaults.reasoningEffort ?? "medium" },
@@ -10279,11 +11174,8 @@ ${error.stack}` : String(error)}` }
10279
11174
  onSelect: async (session) => {
10280
11175
  try {
10281
11176
  const restored = await loadSessionHistory(workspacePath, session.id);
10282
- startTransition2(() => {
10283
- setMessageRenderVersion((current) => current + 1);
10284
- setMessages(restored.messages);
10285
- setHistory(restored.llmHistory);
10286
- });
11177
+ replaceMessages(restored.messages);
11178
+ startTransition2(() => setHistory(restored.llmHistory));
10287
11179
  addSystemMessage(`Resumed session (${session.turnCount} turns). Continue where you left off.`);
10288
11180
  } catch (err) {
10289
11181
  addSystemMessage(`Failed: ${err instanceof Error ? err.message : String(err)}`);
@@ -10316,23 +11208,28 @@ ${error.stack}` : String(error)}` }
10316
11208
  width: contentWidth
10317
11209
  }
10318
11210
  ),
10319
- deferredMessages.length > 0 && /* @__PURE__ */ jsxs4(Box5, { flexDirection: "column", marginBottom: 1, width: contentWidth, children: [
10320
- /* @__PURE__ */ jsx6(Static, { items: staticMessages, children: (message, idx) => renderConversationMessage(message, `static-msg-${idx}`, toolActivityExpanded, contentWidth) }, `conversation-static-${messageRenderVersion}-${toolActivityExpanded ? "e" : "c"}`),
10321
- dynamicMessages.map(
10322
- (message, idx) => renderConversationMessage(message, `dynamic-msg-${staticMessages.length + idx}`, toolActivityExpanded, contentWidth)
10323
- )
10324
- ] }),
10325
- 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(
10326
11222
  SubAgentIndicator,
10327
11223
  {
10328
- agentType: subAgentProgress.agentType,
10329
- goal: subAgentProgress.goal,
10330
- currentTool: subAgentProgress.currentTool,
10331
- toolCount: subAgentProgress.toolCount,
11224
+ agentType: progress.agentType,
11225
+ goal: progress.goal,
11226
+ currentTool: progress.currentTool,
11227
+ toolCount: progress.toolCount,
10332
11228
  frame: activityFrame,
10333
11229
  width: contentWidth
10334
- }
10335
- ),
11230
+ },
11231
+ progress.agentId
11232
+ )),
10336
11233
  taskPanelVisible && getVisibleTasks().length > 0 && /* @__PURE__ */ jsx6(
10337
11234
  TaskPanel,
10338
11235
  {
@@ -10341,11 +11238,28 @@ ${error.stack}` : String(error)}` }
10341
11238
  width: contentWidth
10342
11239
  }
10343
11240
  ),
10344
- deferredPendingUpdates.length > 0 && /* @__PURE__ */ jsx6(
11241
+ visiblePendingUpdates.length > 0 && /* @__PURE__ */ jsx6(
10345
11242
  PendingUpdateCard,
10346
11243
  {
10347
- count: deferredPendingUpdates.length,
10348
- 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
+ },
10349
11263
  width: contentWidth
10350
11264
  }
10351
11265
  ),
@@ -10389,7 +11303,23 @@ ${error.stack}` : String(error)}` }
10389
11303
  ] }) })
10390
11304
  ] }),
10391
11305
  dropdownVisible && /* @__PURE__ */ jsx6(SuggestionDropdown, { width: contentWidth, items: suggestions, selectedIndex: selectedSuggestion }),
10392
- 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
+ ),
10393
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: [
10394
11324
  /* @__PURE__ */ jsx6(PromptPrefix, { busy, frame: activityFrame, hasQuestion: !!agentQuestion, mode: agentMode }),
10395
11325
  /* @__PURE__ */ jsx6(
@@ -10405,7 +11335,7 @@ ${error.stack}` : String(error)}` }
10405
11335
  cursorToEnd,
10406
11336
  accentColor: themeColors.accent,
10407
11337
  mutedColor: themeColors.muted,
10408
- 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"
10409
11339
  }
10410
11340
  )
10411
11341
  ] }) }),
@@ -10434,6 +11364,35 @@ ${error.stack}` : String(error)}` }
10434
11364
  ] }) });
10435
11365
  }
10436
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
+
10437
11396
  // src/cli.ts
10438
11397
  var program = new Command();
10439
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) => {
@@ -10442,7 +11401,7 @@ program.name("open-research").version(getPackageVersion()).description("Local-fi
10442
11401
  const project = await loadWorkspaceProject(target);
10443
11402
  const hasProvider = await hasConfiguredProvider();
10444
11403
  render(
10445
- React6.createElement(App, {
11404
+ React7.createElement(App, {
10446
11405
  initialState: {
10447
11406
  authStatus: hasProvider ? "connected" : "missing",
10448
11407
  workspacePath: project ? target : null,
@@ -10450,7 +11409,10 @@ program.name("open-research").version(getPackageVersion()).description("Local-fi
10450
11409
  pendingUpdates: []
10451
11410
  }
10452
11411
  }),
10453
- { exitOnCtrlC: false }
11412
+ {
11413
+ exitOnCtrlC: false,
11414
+ stdout: createStableInkStdout(process.stdout)
11415
+ }
10454
11416
  );
10455
11417
  });
10456
11418
  program.command("init").argument("[workspacePath]").description("Initialize an Open Research workspace.").action(async (workspacePath) => {
@@ -10541,7 +11503,7 @@ skills.command("validate").argument("[nameOrPath]").description("Validate one us
10541
11503
  await ensureOpenResearchConfig();
10542
11504
  const skillDir = nameOrPath ? path19.isAbsolute(nameOrPath) ? nameOrPath : path19.join(getOpenResearchSkillsDir(), nameOrPath) : getOpenResearchSkillsDir();
10543
11505
  const stat = await import("fs/promises").then(
10544
- (fs19) => fs19.stat(skillDir).catch(() => null)
11506
+ (fs18) => fs18.stat(skillDir).catch(() => null)
10545
11507
  );
10546
11508
  if (!stat) {
10547
11509
  throw new Error(`Skill path not found: ${skillDir}`);