oh-my-opencode-slim 1.0.5 → 1.0.7

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/index.js CHANGED
@@ -6246,33 +6246,33 @@ var require_URL = __commonJS((exports, module) => {
6246
6246
  else
6247
6247
  return basepath.substring(0, lastslash + 1) + refpath;
6248
6248
  }
6249
- function remove_dot_segments(path14) {
6250
- if (!path14)
6251
- return path14;
6249
+ function remove_dot_segments(path16) {
6250
+ if (!path16)
6251
+ return path16;
6252
6252
  var output = "";
6253
- while (path14.length > 0) {
6254
- if (path14 === "." || path14 === "..") {
6255
- path14 = "";
6253
+ while (path16.length > 0) {
6254
+ if (path16 === "." || path16 === "..") {
6255
+ path16 = "";
6256
6256
  break;
6257
6257
  }
6258
- var twochars = path14.substring(0, 2);
6259
- var threechars = path14.substring(0, 3);
6260
- var fourchars = path14.substring(0, 4);
6258
+ var twochars = path16.substring(0, 2);
6259
+ var threechars = path16.substring(0, 3);
6260
+ var fourchars = path16.substring(0, 4);
6261
6261
  if (threechars === "../") {
6262
- path14 = path14.substring(3);
6262
+ path16 = path16.substring(3);
6263
6263
  } else if (twochars === "./") {
6264
- path14 = path14.substring(2);
6264
+ path16 = path16.substring(2);
6265
6265
  } else if (threechars === "/./") {
6266
- path14 = "/" + path14.substring(3);
6267
- } else if (twochars === "/." && path14.length === 2) {
6268
- path14 = "/";
6269
- } else if (fourchars === "/../" || threechars === "/.." && path14.length === 3) {
6270
- path14 = "/" + path14.substring(4);
6266
+ path16 = "/" + path16.substring(3);
6267
+ } else if (twochars === "/." && path16.length === 2) {
6268
+ path16 = "/";
6269
+ } else if (fourchars === "/../" || threechars === "/.." && path16.length === 3) {
6270
+ path16 = "/" + path16.substring(4);
6271
6271
  output = output.replace(/\/?[^\/]*$/, "");
6272
6272
  } else {
6273
- var segment = path14.match(/(\/?([^\/]*))/)[0];
6273
+ var segment = path16.match(/(\/?([^\/]*))/)[0];
6274
6274
  output += segment;
6275
- path14 = path14.substring(segment.length);
6275
+ path16 = path16.substring(segment.length);
6276
6276
  }
6277
6277
  }
6278
6278
  return output;
@@ -18150,14 +18150,14 @@ var require_turndown_cjs = __commonJS((exports, module) => {
18150
18150
  } else if (node.nodeType === 1) {
18151
18151
  replacement = replacementForNode.call(self, node);
18152
18152
  }
18153
- return join12(output, replacement);
18153
+ return join13(output, replacement);
18154
18154
  }, "");
18155
18155
  }
18156
18156
  function postProcess(output) {
18157
18157
  var self = this;
18158
18158
  this.rules.forEach(function(rule) {
18159
18159
  if (typeof rule.append === "function") {
18160
- output = join12(output, rule.append(self.options));
18160
+ output = join13(output, rule.append(self.options));
18161
18161
  }
18162
18162
  });
18163
18163
  return output.replace(/^[\t\r\n]+/, "").replace(/[\t\r\n\s]+$/, "");
@@ -18170,7 +18170,7 @@ var require_turndown_cjs = __commonJS((exports, module) => {
18170
18170
  content = content.trim();
18171
18171
  return whitespace.leading + rule.replacement(content, node, this.options) + whitespace.trailing;
18172
18172
  }
18173
- function join12(output, replacement) {
18173
+ function join13(output, replacement) {
18174
18174
  var s1 = trimTrailingNewlines(output);
18175
18175
  var s2 = trimLeadingNewlines(replacement);
18176
18176
  var nls = Math.max(output.length - s1.length, replacement.length - s2.length);
@@ -18530,6 +18530,17 @@ var SessionManagerConfigSchema = z2.object({
18530
18530
  readContextMinLines: z2.number().int().min(0).max(1000).default(10),
18531
18531
  readContextMaxFiles: z2.number().int().min(0).max(50).default(8)
18532
18532
  });
18533
+ var DivoomConfigSchema = z2.object({
18534
+ enabled: z2.boolean().default(false),
18535
+ python: z2.string().min(1).default("/Applications/Divoom MiniToo.app/Contents/Resources/.venv/bin/python"),
18536
+ script: z2.string().min(1).default("/Applications/Divoom MiniToo.app/Contents/Resources/tools/divoom_send.py"),
18537
+ size: z2.number().int().min(1).max(1024).default(128),
18538
+ fps: z2.number().int().min(1).max(60).default(8),
18539
+ speed: z2.number().int().min(1).max(1e4).default(125),
18540
+ maxFrames: z2.number().int().min(1).max(500).default(24),
18541
+ posterizeBits: z2.number().int().min(1).max(8).default(3),
18542
+ gifs: z2.record(z2.string(), z2.string().min(1)).optional()
18543
+ });
18533
18544
  var TodoContinuationConfigSchema = z2.object({
18534
18545
  maxContinuations: z2.number().int().min(1).max(50).default(5).describe("Maximum consecutive auto-continuations before stopping to ask user"),
18535
18546
  cooldownMs: z2.number().int().min(0).max(30000).default(3000).describe("Delay in ms before auto-continuing (gives user time to abort)"),
@@ -18570,7 +18581,6 @@ var PluginConfigSchema = z2.object({
18570
18581
  setDefaultAgent: z2.boolean().optional(),
18571
18582
  scoringEngineVersion: z2.enum(["v1", "v2-shadow", "v2"]).optional(),
18572
18583
  balanceProviderUsage: z2.boolean().optional(),
18573
- showStartupToast: z2.boolean().optional().describe("Show the startup activation toast when OpenCode starts. Defaults to true."),
18574
18584
  autoUpdate: z2.boolean().optional().describe("Disable automatic installation of plugin updates when false. Defaults to true."),
18575
18585
  manualPlan: ManualPlanSchema.optional(),
18576
18586
  presets: z2.record(z2.string(), PresetSchema).optional(),
@@ -18582,6 +18592,7 @@ var PluginConfigSchema = z2.object({
18582
18592
  websearch: WebsearchConfigSchema.optional(),
18583
18593
  interview: InterviewConfigSchema.optional(),
18584
18594
  sessionManager: SessionManagerConfigSchema.optional(),
18595
+ divoom: DivoomConfigSchema.optional(),
18585
18596
  todoContinuation: TodoContinuationConfigSchema.optional(),
18586
18597
  fallback: FailoverConfigSchema.optional(),
18587
18598
  council: CouncilConfigSchema.optional()
@@ -18598,20 +18609,49 @@ var PluginConfigSchema = z2.object({
18598
18609
 
18599
18610
  // src/config/loader.ts
18600
18611
  var PROMPTS_DIR_NAME = "oh-my-opencode-slim";
18601
- function loadConfigFromPath(configPath) {
18612
+ function loadConfigFromPath(configPath, options) {
18602
18613
  try {
18603
18614
  const content = fs.readFileSync(configPath, "utf-8");
18604
- const rawConfig = JSON.parse(stripJsonComments(content));
18615
+ let rawConfig;
18616
+ try {
18617
+ rawConfig = JSON.parse(stripJsonComments(content));
18618
+ } catch (error) {
18619
+ const message = error instanceof Error ? error.message : String(error);
18620
+ options?.onWarning?.({
18621
+ path: configPath,
18622
+ kind: "invalid-json",
18623
+ message
18624
+ });
18625
+ if (!options?.silent) {
18626
+ console.warn(`[oh-my-opencode-slim] Invalid JSON in ${configPath}:`, message);
18627
+ }
18628
+ return null;
18629
+ }
18605
18630
  const result = PluginConfigSchema.safeParse(rawConfig);
18606
18631
  if (!result.success) {
18607
- console.warn(`[oh-my-opencode-slim] Invalid config at ${configPath}:`);
18608
- console.warn(result.error.format());
18632
+ options?.onWarning?.({
18633
+ path: configPath,
18634
+ kind: "invalid-schema",
18635
+ message: "Config does not match schema",
18636
+ formatted: result.error.format()
18637
+ });
18638
+ if (!options?.silent) {
18639
+ console.warn(`[oh-my-opencode-slim] Invalid config at ${configPath}:`);
18640
+ console.warn(result.error.format());
18641
+ }
18609
18642
  return null;
18610
18643
  }
18611
18644
  return result.data;
18612
18645
  } catch (error) {
18613
18646
  if (error instanceof Error && "code" in error && error.code !== "ENOENT") {
18614
- console.warn(`[oh-my-opencode-slim] Error reading config from ${configPath}:`, error.message);
18647
+ options?.onWarning?.({
18648
+ path: configPath,
18649
+ kind: "read-error",
18650
+ message: error.message
18651
+ });
18652
+ if (!options?.silent) {
18653
+ console.warn(`[oh-my-opencode-slim] Error reading config from ${configPath}:`, error.message);
18654
+ }
18615
18655
  }
18616
18656
  return null;
18617
18657
  }
@@ -18636,6 +18676,26 @@ function findConfigPathInDirs(configDirs, baseName) {
18636
18676
  }
18637
18677
  return null;
18638
18678
  }
18679
+ function findPluginConfigPaths(directory) {
18680
+ const userConfigPath = findConfigPathInDirs(getConfigSearchDirs(), "oh-my-opencode-slim");
18681
+ const projectConfigBasePath = path.join(directory, ".opencode", "oh-my-opencode-slim");
18682
+ const projectConfigPath = findConfigPath(projectConfigBasePath);
18683
+ return { userConfigPath, projectConfigPath };
18684
+ }
18685
+ function mergePluginConfigs(base, override) {
18686
+ return {
18687
+ ...base,
18688
+ ...override,
18689
+ agents: deepMerge(base.agents, override.agents),
18690
+ tmux: deepMerge(base.tmux, override.tmux),
18691
+ multiplexer: deepMerge(base.multiplexer, override.multiplexer),
18692
+ interview: deepMerge(base.interview, override.interview),
18693
+ sessionManager: deepMerge(base.sessionManager, override.sessionManager),
18694
+ divoom: deepMerge(base.divoom, override.divoom),
18695
+ fallback: deepMerge(base.fallback, override.fallback),
18696
+ council: deepMerge(base.council, override.council)
18697
+ };
18698
+ }
18639
18699
  function deepMerge(base, override) {
18640
18700
  if (!base)
18641
18701
  return override;
@@ -18653,24 +18713,12 @@ function deepMerge(base, override) {
18653
18713
  }
18654
18714
  return result;
18655
18715
  }
18656
- function loadPluginConfig(directory) {
18657
- const userConfigPath = findConfigPathInDirs(getConfigSearchDirs(), "oh-my-opencode-slim");
18658
- const projectConfigBasePath = path.join(directory, ".opencode", "oh-my-opencode-slim");
18659
- const projectConfigPath = findConfigPath(projectConfigBasePath);
18660
- let config = userConfigPath ? loadConfigFromPath(userConfigPath) ?? {} : {};
18661
- const projectConfig = projectConfigPath ? loadConfigFromPath(projectConfigPath) : null;
18716
+ function loadPluginConfig(directory, options) {
18717
+ const { userConfigPath, projectConfigPath } = findPluginConfigPaths(directory);
18718
+ let config = userConfigPath ? loadConfigFromPath(userConfigPath, options) ?? {} : {};
18719
+ const projectConfig = projectConfigPath ? loadConfigFromPath(projectConfigPath, options) : null;
18662
18720
  if (projectConfig) {
18663
- config = {
18664
- ...config,
18665
- ...projectConfig,
18666
- agents: deepMerge(config.agents, projectConfig.agents),
18667
- tmux: deepMerge(config.tmux, projectConfig.tmux),
18668
- multiplexer: deepMerge(config.multiplexer, projectConfig.multiplexer),
18669
- interview: deepMerge(config.interview, projectConfig.interview),
18670
- sessionManager: deepMerge(config.sessionManager, projectConfig.sessionManager),
18671
- fallback: deepMerge(config.fallback, projectConfig.fallback),
18672
- council: deepMerge(config.council, projectConfig.council)
18673
- };
18721
+ config = mergePluginConfigs(config, projectConfig);
18674
18722
  }
18675
18723
  config = migrateTmuxToMultiplexer(config);
18676
18724
  const envPreset = process.env.OH_MY_OPENCODE_SLIM_PRESET;
@@ -18684,7 +18732,15 @@ function loadPluginConfig(directory) {
18684
18732
  } else {
18685
18733
  const presetSource = envPreset === config.preset ? "environment variable" : "config file";
18686
18734
  const availablePresets = config.presets ? Object.keys(config.presets).join(", ") : "none";
18687
- console.warn(`[oh-my-opencode-slim] Preset "${config.preset}" not found (from ${presetSource}). Available presets: ${availablePresets}`);
18735
+ const message = `Preset "${config.preset}" not found (from ${presetSource}). Available presets: ${availablePresets}`;
18736
+ options?.onWarning?.({
18737
+ path: projectConfigPath ?? userConfigPath ?? "",
18738
+ kind: "missing-preset",
18739
+ message
18740
+ });
18741
+ if (!options?.silent) {
18742
+ console.warn(`[oh-my-opencode-slim] ${message}`);
18743
+ }
18688
18744
  }
18689
18745
  }
18690
18746
  return config;
@@ -18745,6 +18801,34 @@ function getCustomAgentNames(config) {
18745
18801
  });
18746
18802
  }
18747
18803
  // src/utils/session.ts
18804
+ var SESSION_ABORT_TIMEOUT_MS = 1000;
18805
+
18806
+ class OperationTimeoutError extends Error {
18807
+ constructor(message) {
18808
+ super(message);
18809
+ this.name = "OperationTimeoutError";
18810
+ }
18811
+ }
18812
+ async function withTimeout(operation, timeoutMs, message) {
18813
+ if (timeoutMs <= 0)
18814
+ return operation;
18815
+ let timer;
18816
+ try {
18817
+ return await Promise.race([
18818
+ operation,
18819
+ new Promise((_, reject) => {
18820
+ timer = setTimeout(() => {
18821
+ reject(new OperationTimeoutError(message));
18822
+ }, timeoutMs);
18823
+ })
18824
+ ]);
18825
+ } finally {
18826
+ clearTimeout(timer);
18827
+ }
18828
+ }
18829
+ async function abortSessionWithTimeout(client, sessionId, timeoutMs = SESSION_ABORT_TIMEOUT_MS) {
18830
+ await withTimeout(client.session.abort({ path: { id: sessionId } }), timeoutMs, `Session abort timed out after ${timeoutMs}ms`);
18831
+ }
18748
18832
  function shortModelLabel(model) {
18749
18833
  return model.split("/").pop() ?? model;
18750
18834
  }
@@ -18772,11 +18856,17 @@ async function promptWithTimeout(client, args, timeoutMs) {
18772
18856
  promptPromise,
18773
18857
  new Promise((_, reject) => {
18774
18858
  timer = setTimeout(() => {
18775
- client.session.abort({ path: { id: sessionId } }).catch(() => {});
18776
- reject(new Error(`Prompt timed out after ${timeoutMs}ms`));
18859
+ reject(new OperationTimeoutError(`Prompt timed out after ${timeoutMs}ms`));
18777
18860
  }, timeoutMs);
18778
18861
  })
18779
18862
  ]);
18863
+ } catch (error) {
18864
+ if (error instanceof OperationTimeoutError) {
18865
+ try {
18866
+ await abortSessionWithTimeout(client, sessionId);
18867
+ } catch {}
18868
+ }
18869
+ throw error;
18780
18870
  } finally {
18781
18871
  clearTimeout(timer);
18782
18872
  }
@@ -18823,7 +18913,7 @@ var AGENT_DESCRIPTIONS = {
18823
18913
  - **Don't delegate when:** Know the path and need actual content • Need full file anyway • Single specific lookup • About to edit the file`,
18824
18914
  librarian: `@librarian
18825
18915
  - Role: Authoritative source for current library docs and API references
18826
- - Permissions: None
18916
+ - Permissions: External docs/search MCPs; no file edits
18827
18917
  - Stats: 10x better finding up-to-date library docs than orchestrator, 1/2 cost of orchestrator
18828
18918
  - Capabilities: Fetches latest official docs, examples, API signatures, version-specific behavior via grep_app MCP
18829
18919
  - **Delegate when:** Libraries with frequent API changes (React, Next.js, AI SDKs) • Complex APIs needing official examples (ORMs, auth) • Version-specific behavior matters • Unfamiliar library • Edge cases or advanced features • Nuanced best practices
@@ -19208,7 +19298,8 @@ function createCouncillorAgent(model, customPrompt, customAppendPrompt) {
19208
19298
  grep: "allow",
19209
19299
  lsp: "allow",
19210
19300
  list: "allow",
19211
- codesearch: "allow"
19301
+ codesearch: "allow",
19302
+ ast_grep_search: "allow"
19212
19303
  }
19213
19304
  }
19214
19305
  };
@@ -19530,10 +19621,14 @@ ${customAppendPrompt}`;
19530
19621
 
19531
19622
  // src/agents/index.ts
19532
19623
  var COUNCIL_TOOL_ALLOWED_AGENTS = new Set(["council"]);
19624
+ var SAFE_AGENT_ALIAS_RE = /^[a-z][a-z0-9_-]*$/i;
19533
19625
  function normalizeDisplayName(displayName) {
19534
19626
  const trimmed = displayName.trim();
19535
19627
  return trimmed.startsWith("@") ? trimmed.slice(1) : trimmed;
19536
19628
  }
19629
+ function isSafeDisplayName(displayName) {
19630
+ return SAFE_AGENT_ALIAS_RE.test(displayName);
19631
+ }
19537
19632
  function escapeRegExp(value) {
19538
19633
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
19539
19634
  }
@@ -19567,7 +19662,7 @@ function normalizeCustomAgentName(name) {
19567
19662
  return name.trim();
19568
19663
  }
19569
19664
  function isSafeCustomAgentName(name) {
19570
- return /^[a-z][a-z0-9_-]*$/i.test(name) && !isKnownAgentName(name);
19665
+ return SAFE_AGENT_ALIAS_RE.test(name) && !isKnownAgentName(name);
19571
19666
  }
19572
19667
  function hasCustomAgentModel(override) {
19573
19668
  if (!override?.model) {
@@ -19627,6 +19722,9 @@ var SUBAGENT_FACTORIES = {
19627
19722
  };
19628
19723
  function createAgents(config) {
19629
19724
  const disabled = getDisabledAgents(config);
19725
+ if (!config?.council) {
19726
+ disabled.add("council");
19727
+ }
19630
19728
  const getModelForAgent = (name) => {
19631
19729
  if (name === "fixer" && !getAgentOverride(config, "fixer")?.model) {
19632
19730
  const librarianOverride = getAgentOverride(config, "librarian")?.model;
@@ -19713,6 +19811,9 @@ function createAgents(config) {
19713
19811
  const usedDisplayNames = new Set;
19714
19812
  for (const [, displayName] of displayNameMap) {
19715
19813
  const normalizedDisplayName = normalizeDisplayName(displayName);
19814
+ if (!isSafeDisplayName(normalizedDisplayName)) {
19815
+ throw new Error(`displayName '${normalizedDisplayName}' must match /^[a-z][a-z0-9_-]*$/i`);
19816
+ }
19716
19817
  if (usedDisplayNames.has(normalizedDisplayName)) {
19717
19818
  throw new Error(`Duplicate displayName '${normalizedDisplayName}' assigned to multiple agents`);
19718
19819
  }
@@ -20139,6 +20240,286 @@ class CouncilManager {
20139
20240
  };
20140
20241
  }
20141
20242
  }
20243
+ // src/divoom/manager.ts
20244
+ import { spawn } from "node:child_process";
20245
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2 } from "node:fs";
20246
+ import * as os2 from "node:os";
20247
+ import path3 from "node:path";
20248
+ import { fileURLToPath } from "node:url";
20249
+ var AGENT_GIFS = {
20250
+ council: "council.gif",
20251
+ councillor: "council.gif",
20252
+ designer: "designer.gif",
20253
+ explorer: "explorer.gif",
20254
+ fixer: "fixer.gif",
20255
+ input: "input.gif",
20256
+ intro: "intro.gif",
20257
+ librarian: "librarian.gif",
20258
+ oracle: "oracle.gif",
20259
+ orchestrator: "orchestrator.gif"
20260
+ };
20261
+ var DEFAULT_DIVOOM_CONFIG = {
20262
+ enabled: false,
20263
+ python: "/Applications/Divoom MiniToo.app/Contents/Resources/.venv/bin/python",
20264
+ script: "/Applications/Divoom MiniToo.app/Contents/Resources/tools/divoom_send.py",
20265
+ size: 128,
20266
+ fps: 8,
20267
+ speed: 125,
20268
+ maxFrames: 24,
20269
+ posterizeBits: 3
20270
+ };
20271
+ var DIVOOM_ENABLE_ENV = "OH_MY_OPENCODE_SLIM_DIVOOM";
20272
+ function resolveAssetDir() {
20273
+ const moduleDir = path3.dirname(fileURLToPath(import.meta.url));
20274
+ const candidates = [
20275
+ moduleDir,
20276
+ path3.resolve(moduleDir, "divoom"),
20277
+ path3.resolve(moduleDir, "../../src/divoom"),
20278
+ path3.resolve(process.cwd(), "src/divoom")
20279
+ ];
20280
+ for (const candidate of candidates) {
20281
+ if (existsSync2(path3.join(candidate, "intro.gif"))) {
20282
+ return candidate;
20283
+ }
20284
+ }
20285
+ return null;
20286
+ }
20287
+ function normalizeAgentName(value) {
20288
+ if (typeof value !== "string")
20289
+ return null;
20290
+ const normalized = value.trim().toLowerCase();
20291
+ return normalized.length > 0 ? normalized : null;
20292
+ }
20293
+ function isEnvEnabled(value) {
20294
+ if (!value)
20295
+ return false;
20296
+ return ["1", "true", "yes", "on"].includes(value.trim().toLowerCase());
20297
+ }
20298
+ function inputKey(sessionId, requestId) {
20299
+ return `${sessionId}:${requestId}`;
20300
+ }
20301
+ function getDivoomOutDir(homeDir = os2.homedir()) {
20302
+ const xdg = process.env.XDG_DATA_HOME?.trim();
20303
+ const baseDir = xdg && xdg.length > 0 && path3.isAbsolute(xdg) ? xdg : path3.join(homeDir, ".local", "share");
20304
+ return path3.join(baseDir, "opencode", "storage", "oh-my-opencode-slim", "divoom", "captures");
20305
+ }
20306
+
20307
+ class DivoomManager {
20308
+ sender;
20309
+ assetDir;
20310
+ config;
20311
+ parentStates = new Map;
20312
+ pendingUserInputs = new Set;
20313
+ orchestratorBusy = false;
20314
+ latestRequestedGifPath;
20315
+ lastGifPath;
20316
+ sendQueue = Promise.resolve();
20317
+ constructor(config, sender = defaultSender, options = {}) {
20318
+ this.sender = sender;
20319
+ this.config = {
20320
+ ...DEFAULT_DIVOOM_CONFIG,
20321
+ ...config,
20322
+ enabled: isEnvEnabled(process.env[DIVOOM_ENABLE_ENV]) ? true : config?.enabled ?? false,
20323
+ gifs: config?.gifs
20324
+ };
20325
+ this.assetDir = options.assetDir ?? resolveAssetDir();
20326
+ if (options.sender) {
20327
+ this.sender = options.sender;
20328
+ }
20329
+ }
20330
+ onPluginLoad() {
20331
+ this.show("intro");
20332
+ }
20333
+ onTaskStart(input) {
20334
+ if (!input.parentSessionId || !input.callId)
20335
+ return;
20336
+ if (!isTaskArgs(input.args))
20337
+ return;
20338
+ const agent = normalizeAgentName(input.args.subagent_type);
20339
+ if (!agent)
20340
+ return;
20341
+ const state = this.getParentState(input.parentSessionId);
20342
+ const wasIdle = state.activeCalls.size === 0;
20343
+ state.activeCalls.set(input.callId, agent);
20344
+ this.orchestratorBusy = true;
20345
+ if (wasIdle && !state.displayedAgent) {
20346
+ state.displayedAgent = agent;
20347
+ }
20348
+ this.render();
20349
+ }
20350
+ onTaskEnd(input) {
20351
+ if (!input.parentSessionId || !input.callId)
20352
+ return;
20353
+ const state = this.parentStates.get(input.parentSessionId);
20354
+ if (!state)
20355
+ return;
20356
+ state.activeCalls.delete(input.callId);
20357
+ if (state.activeCalls.size === 0) {
20358
+ this.parentStates.delete(input.parentSessionId);
20359
+ }
20360
+ this.render();
20361
+ }
20362
+ onUserInputRequired(input) {
20363
+ if (!input.sessionId || !input.requestId)
20364
+ return;
20365
+ this.pendingUserInputs.add(inputKey(input.sessionId, input.requestId));
20366
+ this.render();
20367
+ }
20368
+ onUserInputResolved(input) {
20369
+ if (!input.sessionId || !input.requestId)
20370
+ return;
20371
+ this.pendingUserInputs.delete(inputKey(input.sessionId, input.requestId));
20372
+ this.render();
20373
+ }
20374
+ onOrchestratorStatus(input) {
20375
+ if (!input.sessionId || !input.isOrchestrator)
20376
+ return;
20377
+ if (input.status === "busy") {
20378
+ this.orchestratorBusy = true;
20379
+ this.render();
20380
+ return;
20381
+ }
20382
+ if (input.status === "idle") {
20383
+ this.orchestratorBusy = false;
20384
+ this.parentStates.delete(input.sessionId);
20385
+ this.render();
20386
+ }
20387
+ }
20388
+ onSessionDeleted(input) {
20389
+ const sessionId = input.sessionId;
20390
+ if (!sessionId)
20391
+ return;
20392
+ if (input.isOrchestrator)
20393
+ this.orchestratorBusy = false;
20394
+ this.parentStates.delete(sessionId);
20395
+ this.pendingUserInputs = new Set(Array.from(this.pendingUserInputs).filter((key) => !key.startsWith(`${sessionId}:`)));
20396
+ this.render();
20397
+ }
20398
+ render() {
20399
+ if (this.pendingUserInputs.size > 0) {
20400
+ this.show("input");
20401
+ return;
20402
+ }
20403
+ const activeAgent = Array.from(this.parentStates.values()).find((state) => state.displayedAgent && state.activeCalls.size > 0)?.displayedAgent;
20404
+ if (activeAgent) {
20405
+ this.show(activeAgent);
20406
+ return;
20407
+ }
20408
+ this.show(this.orchestratorBusy ? "orchestrator" : "intro");
20409
+ }
20410
+ getParentState(parentSessionId) {
20411
+ const existing = this.parentStates.get(parentSessionId);
20412
+ if (existing)
20413
+ return existing;
20414
+ const created = {
20415
+ activeCalls: new Map
20416
+ };
20417
+ this.parentStates.set(parentSessionId, created);
20418
+ return created;
20419
+ }
20420
+ show(agent) {
20421
+ if (!this.config.enabled)
20422
+ return;
20423
+ if (!this.assetDir) {
20424
+ log("[divoom] asset directory not found");
20425
+ return;
20426
+ }
20427
+ const fileName = this.config.gifs?.[agent] ?? AGENT_GIFS[agent] ?? AGENT_GIFS.orchestrator;
20428
+ const requestedGifPath = path3.isAbsolute(fileName) ? fileName : path3.join(this.assetDir, fileName);
20429
+ const fallbackGifPath = path3.join(this.assetDir, AGENT_GIFS.intro);
20430
+ const gifPath = existsSync2(requestedGifPath) ? requestedGifPath : agent === "input" ? fallbackGifPath : requestedGifPath;
20431
+ if (!existsSync2(gifPath)) {
20432
+ log("[divoom] gif not found", { agent, gifPath: requestedGifPath });
20433
+ return;
20434
+ }
20435
+ if (gifPath === this.latestRequestedGifPath)
20436
+ return;
20437
+ this.latestRequestedGifPath = gifPath;
20438
+ const outDir = getDivoomOutDir();
20439
+ try {
20440
+ mkdirSync2(outDir, { recursive: true });
20441
+ } catch (error) {
20442
+ this.clearLatestIfCurrent(gifPath);
20443
+ log("[divoom] output directory not writable", {
20444
+ outDir,
20445
+ error: String(error)
20446
+ });
20447
+ return;
20448
+ }
20449
+ const call = {
20450
+ command: this.config.python,
20451
+ args: [
20452
+ this.config.script,
20453
+ gifPath,
20454
+ "--size",
20455
+ String(this.config.size),
20456
+ "--fps",
20457
+ String(this.config.fps),
20458
+ "--speed",
20459
+ String(this.config.speed),
20460
+ "--max-frames",
20461
+ String(this.config.maxFrames),
20462
+ "--posterize-bits",
20463
+ String(this.config.posterizeBits),
20464
+ "--out-dir",
20465
+ outDir
20466
+ ]
20467
+ };
20468
+ this.sendQueue = this.sendQueue.catch(() => {}).then(async () => {
20469
+ if (gifPath !== this.latestRequestedGifPath)
20470
+ return;
20471
+ if (!existsSync2(this.config.python)) {
20472
+ this.clearLatestIfCurrent(gifPath);
20473
+ log("[divoom] python executable not found", this.config.python);
20474
+ return;
20475
+ }
20476
+ if (!existsSync2(this.config.script)) {
20477
+ this.clearLatestIfCurrent(gifPath);
20478
+ log("[divoom] sender script not found", this.config.script);
20479
+ return;
20480
+ }
20481
+ try {
20482
+ await this.sender(call);
20483
+ this.lastGifPath = gifPath;
20484
+ log("[divoom] showing gif", { agent, gifPath });
20485
+ } catch (error) {
20486
+ this.clearLatestIfCurrent(gifPath);
20487
+ log("[divoom] failed to send gif", String(error));
20488
+ }
20489
+ });
20490
+ }
20491
+ async flush() {
20492
+ await this.sendQueue.catch(() => {});
20493
+ }
20494
+ clearLatestIfCurrent(gifPath) {
20495
+ if (this.latestRequestedGifPath === gifPath) {
20496
+ this.latestRequestedGifPath = undefined;
20497
+ }
20498
+ if (this.lastGifPath === gifPath) {
20499
+ this.lastGifPath = undefined;
20500
+ }
20501
+ }
20502
+ }
20503
+ function isTaskArgs(value) {
20504
+ return typeof value === "object" && value !== null;
20505
+ }
20506
+ function defaultSender(call) {
20507
+ return new Promise((resolve, reject) => {
20508
+ const child = spawn(call.command, call.args, {
20509
+ detached: true,
20510
+ stdio: "ignore"
20511
+ });
20512
+ child.once("error", reject);
20513
+ child.once("spawn", () => {
20514
+ child.unref();
20515
+ resolve();
20516
+ });
20517
+ });
20518
+ }
20519
+ function createDivoomManager(config) {
20520
+ return new DivoomManager(config);
20521
+ }
20522
+
20142
20523
  // src/hooks/apply-patch/errors.ts
20143
20524
  var APPLY_PATCH_ERROR_PREFIX = {
20144
20525
  blocked: "apply_patch blocked",
@@ -20207,7 +20588,7 @@ function ensureApplyPatchError(error, context) {
20207
20588
 
20208
20589
  // src/hooks/apply-patch/execution-context.ts
20209
20590
  import * as fs3 from "node:fs/promises";
20210
- import path3 from "node:path";
20591
+ import path4 from "node:path";
20211
20592
 
20212
20593
  // src/hooks/apply-patch/codec.ts
20213
20594
  function normalizeLineEndings(text) {
@@ -21076,7 +21457,7 @@ function isMissingPathError(error) {
21076
21457
  }
21077
21458
  async function real(target) {
21078
21459
  const parts = [];
21079
- let current = path3.resolve(target);
21460
+ let current = path4.resolve(target);
21080
21461
  while (true) {
21081
21462
  const exact = await fs3.realpath(current).catch((error) => {
21082
21463
  if (isMissingPathError(error)) {
@@ -21085,19 +21466,19 @@ async function real(target) {
21085
21466
  throw createApplyPatchInternalError(`Failed to resolve real path: ${current}`, error);
21086
21467
  });
21087
21468
  if (exact) {
21088
- return parts.length === 0 ? exact : path3.join(exact, ...parts.reverse());
21469
+ return parts.length === 0 ? exact : path4.join(exact, ...parts.reverse());
21089
21470
  }
21090
- const parent = path3.dirname(current);
21471
+ const parent = path4.dirname(current);
21091
21472
  if (parent === current) {
21092
- return parts.length === 0 ? current : path3.join(current, ...parts.reverse());
21473
+ return parts.length === 0 ? current : path4.join(current, ...parts.reverse());
21093
21474
  }
21094
- parts.push(path3.basename(current));
21475
+ parts.push(path4.basename(current));
21095
21476
  current = parent;
21096
21477
  }
21097
21478
  }
21098
21479
  function inside(root, target) {
21099
- const rel = path3.relative(root, target);
21100
- return rel === "" || !rel.startsWith("..") && !path3.isAbsolute(rel);
21480
+ const rel = path4.relative(root, target);
21481
+ return rel === "" || !rel.startsWith("..") && !path4.isAbsolute(rel);
21101
21482
  }
21102
21483
  function createPathGuardContext(root, worktree) {
21103
21484
  return {
@@ -21107,7 +21488,7 @@ function createPathGuardContext(root, worktree) {
21107
21488
  };
21108
21489
  }
21109
21490
  async function realCached(ctx, target) {
21110
- const resolvedTarget = path3.resolve(target);
21491
+ const resolvedTarget = path4.resolve(target);
21111
21492
  let pending = ctx.realCache.get(resolvedTarget);
21112
21493
  if (!pending) {
21113
21494
  pending = real(resolvedTarget);
@@ -21158,22 +21539,22 @@ async function assertRegularFile(ctx, filePath, verb) {
21158
21539
  function collectPatchTargets(root, hunks) {
21159
21540
  const targets = new Set;
21160
21541
  for (const hunk of hunks) {
21161
- targets.add(path3.resolve(root, hunk.path));
21542
+ targets.add(path4.resolve(root, hunk.path));
21162
21543
  if (hunk.type === "update" && hunk.move_path) {
21163
- targets.add(path3.resolve(root, hunk.move_path));
21544
+ targets.add(path4.resolve(root, hunk.move_path));
21164
21545
  }
21165
21546
  }
21166
21547
  return [...targets];
21167
21548
  }
21168
21549
  function toRelativePatchPath(root, target) {
21169
- const relative = path3.relative(root, target);
21550
+ const relative = path4.relative(root, target);
21170
21551
  return (relative.length === 0 ? "." : relative).replaceAll("\\", "/");
21171
21552
  }
21172
21553
  function normalizePatchPath(root, value) {
21173
- return path3.isAbsolute(value) ? toRelativePatchPath(root, path3.resolve(value)) : value;
21554
+ return path4.isAbsolute(value) ? toRelativePatchPath(root, path4.resolve(value)) : value;
21174
21555
  }
21175
21556
  function normalizePatchPaths(root, hunks) {
21176
- const resolvedRoot = path3.resolve(root);
21557
+ const resolvedRoot = path4.resolve(root);
21177
21558
  const normalized = [];
21178
21559
  let changed = false;
21179
21560
  for (const hunk of hunks) {
@@ -21297,7 +21678,7 @@ function stageAddedText(contents) {
21297
21678
  `;
21298
21679
  }
21299
21680
  // src/hooks/apply-patch/rewrite.ts
21300
- import path4 from "node:path";
21681
+ import path5 from "node:path";
21301
21682
  function normalizeTextLineEndings(text) {
21302
21683
  return text.replace(/\r\n/g, `
21303
21684
  `).replace(/\r/g, `
@@ -21454,7 +21835,7 @@ async function rewritePatch(root, patchText, cfg, worktree) {
21454
21835
  const dependencyGroups = new Map;
21455
21836
  for (const hunk of hunks) {
21456
21837
  if (hunk.type === "add") {
21457
- const filePath2 = path4.resolve(root, hunk.path);
21838
+ const filePath2 = path5.resolve(root, hunk.path);
21458
21839
  await assertPreparedPathMissing(filePath2, "add");
21459
21840
  rewritten.push(hunk);
21460
21841
  clearDependencyGroup(filePath2);
@@ -21476,20 +21857,20 @@ async function rewritePatch(root, patchText, cfg, worktree) {
21476
21857
  continue;
21477
21858
  }
21478
21859
  if (hunk.type === "delete") {
21479
- const filePath2 = path4.resolve(root, hunk.path);
21860
+ const filePath2 = path5.resolve(root, hunk.path);
21480
21861
  await getPreparedFileState(filePath2, "delete");
21481
21862
  clearDependencyGroup(filePath2);
21482
21863
  rewritten.push(hunk);
21483
21864
  staged.set(filePath2, { exists: false, derived: true });
21484
21865
  continue;
21485
21866
  }
21486
- const filePath = path4.resolve(root, hunk.path);
21867
+ const filePath = path5.resolve(root, hunk.path);
21487
21868
  const currentDependency = dependencyGroups.get(filePath);
21488
21869
  const current = await getPreparedFileState(filePath, "update");
21489
21870
  if (!current.exists) {
21490
21871
  throw createApplyPatchVerificationError(`Failed to read file to update: ${filePath}`);
21491
21872
  }
21492
- const movePath = hunk.move_path ? path4.resolve(root, hunk.move_path) : undefined;
21873
+ const movePath = hunk.move_path ? path5.resolve(root, hunk.move_path) : undefined;
21493
21874
  if (movePath && movePath !== filePath) {
21494
21875
  await assertPreparedPathMissing(movePath, "move");
21495
21876
  }
@@ -21683,32 +22064,32 @@ function crossSpawn(command, options) {
21683
22064
  }
21684
22065
  };
21685
22066
  }
21686
- async function crossWrite(path5, data) {
21687
- await fsWriteFile(path5, Buffer.from(data));
22067
+ async function crossWrite(path6, data) {
22068
+ await fsWriteFile(path6, Buffer.from(data));
21688
22069
  }
21689
22070
 
21690
22071
  // src/hooks/auto-update-checker/cache.ts
21691
22072
  import * as fs5 from "node:fs";
21692
- import * as path7 from "node:path";
22073
+ import * as path8 from "node:path";
21693
22074
  // src/hooks/auto-update-checker/checker.ts
21694
22075
  import * as fs4 from "node:fs";
21695
- import * as path6 from "node:path";
21696
- import { fileURLToPath } from "node:url";
22076
+ import * as path7 from "node:path";
22077
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
21697
22078
 
21698
22079
  // src/hooks/auto-update-checker/constants.ts
21699
- import * as os2 from "node:os";
21700
- import * as path5 from "node:path";
22080
+ import * as os3 from "node:os";
22081
+ import * as path6 from "node:path";
21701
22082
  var PACKAGE_NAME = "oh-my-opencode-slim";
21702
22083
  var NPM_REGISTRY_URL = `https://registry.npmjs.org/-/package/${PACKAGE_NAME}/dist-tags`;
21703
22084
  var NPM_FETCH_TIMEOUT = 5000;
21704
22085
  function getCacheDir() {
21705
22086
  if (process.platform === "win32") {
21706
- return path5.join(process.env.LOCALAPPDATA ?? os2.homedir(), "opencode");
22087
+ return path6.join(process.env.LOCALAPPDATA ?? os3.homedir(), "opencode");
21707
22088
  }
21708
- return path5.join(os2.homedir(), ".cache", "opencode");
22089
+ return path6.join(os3.homedir(), ".cache", "opencode");
21709
22090
  }
21710
22091
  var CACHE_DIR = getCacheDir();
21711
- var INSTALLED_PACKAGE_JSON = path5.join(CACHE_DIR, "node_modules", PACKAGE_NAME, "package.json");
22092
+ var INSTALLED_PACKAGE_JSON = path6.join(CACHE_DIR, "node_modules", PACKAGE_NAME, "package.json");
21712
22093
  var configPaths = getOpenCodeConfigPaths();
21713
22094
  var USER_OPENCODE_CONFIG = configPaths[0];
21714
22095
  var USER_OPENCODE_CONFIG_JSONC = configPaths[1];
@@ -21743,8 +22124,8 @@ function extractChannel(version) {
21743
22124
  }
21744
22125
  function getConfigPaths(directory) {
21745
22126
  return [
21746
- path6.join(directory, ".opencode", "opencode.json"),
21747
- path6.join(directory, ".opencode", "opencode.jsonc"),
22127
+ path7.join(directory, ".opencode", "opencode.json"),
22128
+ path7.join(directory, ".opencode", "opencode.jsonc"),
21748
22129
  USER_OPENCODE_CONFIG,
21749
22130
  USER_OPENCODE_CONFIG_JSONC
21750
22131
  ];
@@ -21760,7 +22141,7 @@ function getLocalDevPath(directory) {
21760
22141
  for (const entry of plugins) {
21761
22142
  if (entry.startsWith("file://") && entry.includes(PACKAGE_NAME)) {
21762
22143
  try {
21763
- return fileURLToPath(entry);
22144
+ return fileURLToPath2(entry);
21764
22145
  } catch {
21765
22146
  return entry.replace("file://", "");
21766
22147
  }
@@ -21773,9 +22154,9 @@ function getLocalDevPath(directory) {
21773
22154
  function findPackageJsonUp(startPath) {
21774
22155
  try {
21775
22156
  const stat2 = fs4.statSync(startPath);
21776
- let dir = stat2.isDirectory() ? startPath : path6.dirname(startPath);
22157
+ let dir = stat2.isDirectory() ? startPath : path7.dirname(startPath);
21777
22158
  for (let i = 0;i < 10; i++) {
21778
- const pkgPath = path6.join(dir, "package.json");
22159
+ const pkgPath = path7.join(dir, "package.json");
21779
22160
  if (fs4.existsSync(pkgPath)) {
21780
22161
  try {
21781
22162
  const content = fs4.readFileSync(pkgPath, "utf-8");
@@ -21784,7 +22165,7 @@ function findPackageJsonUp(startPath) {
21784
22165
  return pkgPath;
21785
22166
  } catch {}
21786
22167
  }
21787
- const parent = path6.dirname(dir);
22168
+ const parent = path7.dirname(dir);
21788
22169
  if (parent === dir)
21789
22170
  break;
21790
22171
  dir = parent;
@@ -21809,7 +22190,7 @@ function getLocalDevVersion(directory) {
21809
22190
  }
21810
22191
  function getCurrentRuntimePackageJsonPath(currentModuleUrl = import.meta.url) {
21811
22192
  try {
21812
- const currentDir = path6.dirname(fileURLToPath(currentModuleUrl));
22193
+ const currentDir = path7.dirname(fileURLToPath2(currentModuleUrl));
21813
22194
  return findPackageJsonUp(currentDir);
21814
22195
  } catch (err) {
21815
22196
  log("[auto-update-checker] Failed to resolve runtime package path:", err);
@@ -21893,7 +22274,7 @@ async function getLatestVersion(channel = "latest") {
21893
22274
 
21894
22275
  // src/hooks/auto-update-checker/cache.ts
21895
22276
  function removeFromBunLock(installDir, packageName) {
21896
- const lockPath = path7.join(installDir, "bun.lock");
22277
+ const lockPath = path8.join(installDir, "bun.lock");
21897
22278
  if (!fs5.existsSync(lockPath))
21898
22279
  return false;
21899
22280
  try {
@@ -21944,7 +22325,7 @@ function ensureDependencyVersion(packageJsonPath, packageName, version) {
21944
22325
  }
21945
22326
  }
21946
22327
  function removeInstalledPackage(installDir, packageName) {
21947
- const pkgDir = path7.join(installDir, "node_modules", packageName);
22328
+ const pkgDir = path8.join(installDir, "node_modules", packageName);
21948
22329
  if (!fs5.existsSync(pkgDir))
21949
22330
  return false;
21950
22331
  fs5.rmSync(pkgDir, { recursive: true, force: true });
@@ -21953,18 +22334,18 @@ function removeInstalledPackage(installDir, packageName) {
21953
22334
  }
21954
22335
  function resolveInstallContext(runtimePackageJsonPath = getCurrentRuntimePackageJsonPath()) {
21955
22336
  if (runtimePackageJsonPath) {
21956
- const packageDir = path7.dirname(runtimePackageJsonPath);
21957
- const nodeModulesDir = path7.dirname(packageDir);
21958
- if (path7.basename(packageDir) === PACKAGE_NAME && path7.basename(nodeModulesDir) === "node_modules") {
21959
- const installDir = path7.dirname(nodeModulesDir);
21960
- const packageJsonPath = path7.join(installDir, "package.json");
22337
+ const packageDir = path8.dirname(runtimePackageJsonPath);
22338
+ const nodeModulesDir = path8.dirname(packageDir);
22339
+ if (path8.basename(packageDir) === PACKAGE_NAME && path8.basename(nodeModulesDir) === "node_modules") {
22340
+ const installDir = path8.dirname(nodeModulesDir);
22341
+ const packageJsonPath = path8.join(installDir, "package.json");
21961
22342
  if (fs5.existsSync(packageJsonPath)) {
21962
22343
  return { installDir, packageJsonPath };
21963
22344
  }
21964
22345
  }
21965
22346
  return null;
21966
22347
  }
21967
- const legacyPackageJsonPath = path7.join(CACHE_DIR, "package.json");
22348
+ const legacyPackageJsonPath = path8.join(CACHE_DIR, "package.json");
21968
22349
  if (fs5.existsSync(legacyPackageJsonPath)) {
21969
22350
  return { installDir: CACHE_DIR, packageJsonPath: legacyPackageJsonPath };
21970
22351
  }
@@ -21995,7 +22376,7 @@ function preparePackageUpdate(version, packageName = PACKAGE_NAME, runtimePackag
21995
22376
 
21996
22377
  // src/hooks/auto-update-checker/index.ts
21997
22378
  function createAutoUpdateCheckerHook(ctx, options = {}) {
21998
- const { showStartupToast = true, autoUpdate = true } = options;
22379
+ const { autoUpdate = true } = options;
21999
22380
  let hasChecked = false;
22000
22381
  return {
22001
22382
  event: ({ event }) => {
@@ -22008,19 +22389,11 @@ function createAutoUpdateCheckerHook(ctx, options = {}) {
22008
22389
  return;
22009
22390
  hasChecked = true;
22010
22391
  setTimeout(async () => {
22011
- const cachedVersion = getCachedVersion();
22012
22392
  const localDevVersion = getLocalDevVersion(ctx.directory);
22013
- const displayVersion = localDevVersion ?? cachedVersion;
22014
22393
  if (localDevVersion) {
22015
- if (showStartupToast) {
22016
- showToast(ctx, `OMO-Slim ${displayVersion} (dev)`, "Running in local development mode.", "info");
22017
- }
22018
22394
  log("[auto-update-checker] Local development mode");
22019
22395
  return;
22020
22396
  }
22021
- if (showStartupToast) {
22022
- showToast(ctx, `OMO-Slim ${displayVersion ?? "unknown"}`, "oh-my-opencode-slim is active.", "info");
22023
- }
22024
22397
  runBackgroundUpdateCheck(ctx, autoUpdate).catch((err) => {
22025
22398
  log("[auto-update-checker] Background update check failed:", err);
22026
22399
  });
@@ -22106,7 +22479,7 @@ function showToast(ctx, title, message, variant = "info", duration = 3000) {
22106
22479
  }).catch(() => {});
22107
22480
  }
22108
22481
  // src/utils/agent-variant.ts
22109
- function normalizeAgentName(agentName) {
22482
+ function normalizeAgentName2(agentName) {
22110
22483
  const trimmed = agentName.trim();
22111
22484
  return trimmed.startsWith("@") ? trimmed.slice(1) : trimmed;
22112
22485
  }
@@ -22118,7 +22491,7 @@ function getRuntimeAgentNames(config) {
22118
22491
  return [...unique];
22119
22492
  }
22120
22493
  function resolveRuntimeAgentName(config, agentName) {
22121
- const normalized = normalizeAgentName(agentName);
22494
+ const normalized = normalizeAgentName2(agentName);
22122
22495
  if (!normalized) {
22123
22496
  return normalized;
22124
22497
  }
@@ -22130,7 +22503,7 @@ function resolveRuntimeAgentName(config, agentName) {
22130
22503
  if (!displayName) {
22131
22504
  continue;
22132
22505
  }
22133
- if (normalizeAgentName(displayName) === normalized) {
22506
+ if (normalizeAgentName2(displayName) === normalized) {
22134
22507
  return internalName;
22135
22508
  }
22136
22509
  }
@@ -22146,7 +22519,7 @@ function createDisplayNameMentionRewriter(config) {
22146
22519
  if (!displayName) {
22147
22520
  continue;
22148
22521
  }
22149
- const normalizedDisplayName = normalizeAgentName(displayName);
22522
+ const normalizedDisplayName = normalizeAgentName2(displayName);
22150
22523
  if (!normalizedDisplayName || normalizedDisplayName === internalName) {
22151
22524
  continue;
22152
22525
  }
@@ -22456,8 +22829,8 @@ function isPwshAvailable() {
22456
22829
  });
22457
22830
  return result.status === 0;
22458
22831
  }
22459
- function escapePowerShellPath(path8) {
22460
- return path8.replace(/'/g, "''");
22832
+ function escapePowerShellPath(path9) {
22833
+ return path9.replace(/'/g, "''");
22461
22834
  }
22462
22835
  function getWindowsZipExtractor() {
22463
22836
  const buildNumber = getWindowsBuildNumber();
@@ -22786,6 +23159,7 @@ function parseModel(model) {
22786
23159
  return { providerID: model.slice(0, slash), modelID: model.slice(slash + 1) };
22787
23160
  }
22788
23161
  var DEDUP_WINDOW_MS = 5000;
23162
+ var REPROMPT_DELAY_MS = 500;
22789
23163
 
22790
23164
  class ForegroundFallbackManager {
22791
23165
  client;
@@ -22918,11 +23292,20 @@ class ForegroundFallbackManager {
22918
23292
  log("[foreground-fallback] no user message found", { sessionID });
22919
23293
  return;
22920
23294
  }
22921
- try {
22922
- await this.client.session.abort({ path: { id: sessionID } });
22923
- } catch {}
22924
- await new Promise((r) => setTimeout(r, 500));
22925
23295
  const sessionClient = this.client.session;
23296
+ if (typeof sessionClient.promptAsync !== "function") {
23297
+ log("[foreground-fallback] promptAsync unavailable", { sessionID });
23298
+ return;
23299
+ }
23300
+ try {
23301
+ await abortSessionWithTimeout(this.client, sessionID);
23302
+ } catch (error) {
23303
+ log("[foreground-fallback] abort did not complete cleanly", {
23304
+ sessionID,
23305
+ error: error instanceof Error ? error.message : String(error)
23306
+ });
23307
+ }
23308
+ await new Promise((r) => setTimeout(r, REPROMPT_DELAY_MS));
22926
23309
  await sessionClient.promptAsync({
22927
23310
  path: { id: sessionID },
22928
23311
  body: { parts: lastUser.parts, model: ref }
@@ -22969,8 +23352,8 @@ class ForegroundFallbackManager {
22969
23352
  // src/hooks/image-hook.ts
22970
23353
  import { createHash } from "node:crypto";
22971
23354
  import {
22972
- existsSync as existsSync4,
22973
- mkdirSync as mkdirSync2,
23355
+ existsSync as existsSync5,
23356
+ mkdirSync as mkdirSync3,
22974
23357
  readdirSync as readdirSync2,
22975
23358
  rmdirSync,
22976
23359
  statSync as statSync3,
@@ -23065,7 +23448,7 @@ function writeUniqueFile(dir, name, data, log2) {
23065
23448
  const ext = extname(name);
23066
23449
  const base = basename2(name, ext) || name;
23067
23450
  let candidate = join7(dir, name);
23068
- if (existsSync4(candidate)) {
23451
+ if (existsSync5(candidate)) {
23069
23452
  return candidate;
23070
23453
  }
23071
23454
  let counter = 0;
@@ -23103,14 +23486,14 @@ function processImageAttachments(args) {
23103
23486
  }
23104
23487
  const saveDir = join7(workDir, ".opencode", "images");
23105
23488
  if (messagesWithImages.length === 0) {
23106
- if (existsSync4(saveDir))
23489
+ if (existsSync5(saveDir))
23107
23490
  cleanupAllSessions(saveDir);
23108
23491
  return;
23109
23492
  }
23110
23493
  const gitignorePath = join7(workDir, ".opencode", ".gitignore");
23111
23494
  try {
23112
- mkdirSync2(saveDir, { recursive: true });
23113
- if (!existsSync4(gitignorePath))
23495
+ mkdirSync3(saveDir, { recursive: true });
23496
+ if (!existsSync5(gitignorePath))
23114
23497
  writeFileSync3(gitignorePath, `*
23115
23498
  `);
23116
23499
  } catch (e) {
@@ -23121,7 +23504,7 @@ function processImageAttachments(args) {
23121
23504
  const sessionSubdir = msg.info.sessionID ? sanitizeFilename(msg.info.sessionID) : undefined;
23122
23505
  const targetDir = sessionSubdir ? join7(saveDir, sessionSubdir) : saveDir;
23123
23506
  try {
23124
- mkdirSync2(targetDir, { recursive: true });
23507
+ mkdirSync3(targetDir, { recursive: true });
23125
23508
  } catch (e) {
23126
23509
  log2(`[image-hook] failed to create target image directory: ${e}`);
23127
23510
  }
@@ -23280,7 +23663,7 @@ function createPostFileToolNudgeHook(options = {}) {
23280
23663
  };
23281
23664
  }
23282
23665
  // src/hooks/task-session-manager/index.ts
23283
- import path8 from "node:path";
23666
+ import path9 from "node:path";
23284
23667
  var AGENT_NAME_SET = new Set([
23285
23668
  "orchestrator",
23286
23669
  "oracle",
@@ -23305,8 +23688,8 @@ function extractPath(output) {
23305
23688
  return /<path>([^<]+)<\/path>/.exec(output)?.[1];
23306
23689
  }
23307
23690
  function normalizePath(root, file) {
23308
- const relative = path8.relative(root, file);
23309
- if (!relative || relative.startsWith("..") || path8.isAbsolute(relative)) {
23691
+ const relative = path9.relative(root, file);
23692
+ if (!relative || relative.startsWith("..") || path9.isAbsolute(relative)) {
23310
23693
  return file;
23311
23694
  }
23312
23695
  return relative;
@@ -23342,6 +23725,7 @@ function createTaskSessionManagerHook(_ctx, options) {
23342
23725
  const pendingCallOrder = [];
23343
23726
  const contextByTask = new Map;
23344
23727
  const pendingManagedTaskIds = new Set;
23728
+ let anonymousPendingCallId = 0;
23345
23729
  function addTaskContext(taskId, files) {
23346
23730
  if (files.length === 0)
23347
23731
  return;
@@ -23388,6 +23772,9 @@ function createTaskSessionManagerHook(_ctx, options) {
23388
23772
  const firstLine = output.split(/\r?\n/, 1)[0]?.trim().toLowerCase() ?? "";
23389
23773
  return firstLine.startsWith("[error]") && firstLine.includes("session") && (firstLine.includes("not found") || firstLine.includes("no session"));
23390
23774
  }
23775
+ function pendingCallId(input) {
23776
+ return input.callID ?? `${input.sessionID ?? "unknown"}:anonymous-${++anonymousPendingCallId}`;
23777
+ }
23391
23778
  function rememberPendingCall(call) {
23392
23779
  const existingIndex = pendingCallOrder.indexOf(call.callId);
23393
23780
  if (existingIndex >= 0) {
@@ -23403,17 +23790,23 @@ function createTaskSessionManagerHook(_ctx, options) {
23403
23790
  pendingCalls.delete(evictedCallId);
23404
23791
  }
23405
23792
  }
23406
- function takePendingCall(callId) {
23407
- if (!callId)
23793
+ function takePendingCall(callId, parentSessionId) {
23794
+ const resolvedCallId = callId ?? firstPendingCallForParent(parentSessionId);
23795
+ if (!resolvedCallId)
23408
23796
  return;
23409
- const pending = pendingCalls.get(callId);
23410
- pendingCalls.delete(callId);
23411
- const orderIndex = pendingCallOrder.indexOf(callId);
23797
+ const pending = pendingCalls.get(resolvedCallId);
23798
+ pendingCalls.delete(resolvedCallId);
23799
+ const orderIndex = pendingCallOrder.indexOf(resolvedCallId);
23412
23800
  if (orderIndex >= 0) {
23413
23801
  pendingCallOrder.splice(orderIndex, 1);
23414
23802
  }
23415
23803
  return pending;
23416
23804
  }
23805
+ function firstPendingCallForParent(parentSessionId) {
23806
+ if (!parentSessionId)
23807
+ return;
23808
+ return pendingCallOrder.find((callId) => pendingCalls.get(callId)?.parentSessionId === parentSessionId);
23809
+ }
23417
23810
  return {
23418
23811
  "tool.execute.before": async (input, output) => {
23419
23812
  if (input.tool.toLowerCase() !== "task")
@@ -23431,14 +23824,16 @@ function createTaskSessionManagerHook(_ctx, options) {
23431
23824
  prompt: typeof args.prompt === "string" ? args.prompt : undefined,
23432
23825
  agentType: args.subagent_type
23433
23826
  });
23434
- if (input.callID) {
23435
- rememberPendingCall({
23436
- callId: input.callID,
23437
- parentSessionId: input.sessionID,
23438
- agentType: args.subagent_type,
23439
- label
23440
- });
23441
- }
23827
+ const pendingCall = {
23828
+ callId: pendingCallId({
23829
+ callID: input.callID,
23830
+ sessionID: input.sessionID
23831
+ }),
23832
+ parentSessionId: input.sessionID,
23833
+ agentType: args.subagent_type,
23834
+ label
23835
+ };
23836
+ rememberPendingCall(pendingCall);
23442
23837
  if (typeof args.task_id !== "string" || args.task_id.trim() === "") {
23443
23838
  return;
23444
23839
  }
@@ -23451,15 +23846,8 @@ function createTaskSessionManagerHook(_ctx, options) {
23451
23846
  args.task_id = remembered.taskId;
23452
23847
  pendingManagedTaskIds.add(remembered.taskId);
23453
23848
  sessionManager.markUsed(input.sessionID, args.subagent_type, remembered.taskId);
23454
- if (input.callID) {
23455
- rememberPendingCall({
23456
- callId: input.callID,
23457
- parentSessionId: input.sessionID,
23458
- agentType: args.subagent_type,
23459
- label,
23460
- resumedTaskId: remembered.taskId
23461
- });
23462
- }
23849
+ pendingCall.resumedTaskId = remembered.taskId;
23850
+ rememberPendingCall(pendingCall);
23463
23851
  },
23464
23852
  "tool.execute.after": async (input, output) => {
23465
23853
  if (input.tool.toLowerCase() === "read") {
@@ -23470,7 +23858,7 @@ function createTaskSessionManagerHook(_ctx, options) {
23470
23858
  }
23471
23859
  if (input.tool.toLowerCase() !== "task")
23472
23860
  return;
23473
- const pending = takePendingCall(input.callID);
23861
+ const pending = takePendingCall(input.callID, input.sessionID);
23474
23862
  if (!pending || typeof output.output !== "string")
23475
23863
  return;
23476
23864
  const taskId = parseTaskIdFromTaskOutput(output.output);
@@ -23538,8 +23926,8 @@ function createTaskSessionManagerHook(_ctx, options) {
23538
23926
  const sessionId = input.event.properties?.info?.id ?? input.event.properties?.sessionID;
23539
23927
  if (!sessionId)
23540
23928
  return;
23541
- sessionManager.clearParent(sessionId);
23542
23929
  sessionManager.dropTask(sessionId);
23930
+ sessionManager.clearParent(sessionId);
23543
23931
  contextByTask.delete(sessionId);
23544
23932
  pendingManagedTaskIds.delete(sessionId);
23545
23933
  pruneContext();
@@ -23693,6 +24081,7 @@ function createTodoHygiene(options) {
23693
24081
  // src/hooks/todo-continuation/index.ts
23694
24082
  var HOOK_NAME = "todo-continuation";
23695
24083
  var COMMAND_NAME = "auto-continue";
24084
+ var TODO_STATE_TIMEOUT_MS = 500;
23696
24085
  var CONTINUATION_PROMPT = "[Auto-continue: enabled - there are incomplete todos remaining. Continue with the next uncompleted item. Press Esc to cancel. If you need user input or review for the next item, ask instead of proceeding.]";
23697
24086
  var TODO_HYGIENE_INSTRUCTION_OPEN = '<instruction name="todo_hygiene">';
23698
24087
  var TODO_HYGIENE_INSTRUCTION_CLOSE = "</instruction>";
@@ -23780,12 +24169,15 @@ function createTodoContinuationHook(ctx, config) {
23780
24169
  notifyingSessionIds: new Set,
23781
24170
  notificationBusyUntilBySession: new Map
23782
24171
  };
24172
+ async function fetchTodos(sessionID) {
24173
+ const result = await withTimeout(ctx.client.session.todo({
24174
+ path: { id: sessionID }
24175
+ }), TODO_STATE_TIMEOUT_MS, `Todo state lookup timed out after ${TODO_STATE_TIMEOUT_MS}ms`);
24176
+ return result.data;
24177
+ }
23783
24178
  const hygiene = createTodoHygiene({
23784
24179
  getTodoState: async (sessionID) => {
23785
- const result = await ctx.client.session.todo({
23786
- path: { id: sessionID }
23787
- });
23788
- const todos = result.data;
24180
+ const todos = await fetchTodos(sessionID);
23789
24181
  const openTodos = todos.filter((todo) => !TERMINAL_TODO_STATUSES.includes(todo.status));
23790
24182
  return {
23791
24183
  hasOpenTodos: openTodos.length > 0,
@@ -23970,10 +24362,7 @@ function createTodoContinuationHook(ctx, config) {
23970
24362
  }
23971
24363
  if (autoEnable && !state.enabled) {
23972
24364
  try {
23973
- const todosResult = await ctx.client.session.todo({
23974
- path: { id: sessionID }
23975
- });
23976
- const todos = todosResult.data;
24365
+ const todos = await fetchTodos(sessionID);
23977
24366
  const incompleteCount2 = todos.filter((t) => !TERMINAL_TODO_STATUSES.includes(t.status)).length;
23978
24367
  if (incompleteCount2 >= autoEnableThreshold) {
23979
24368
  state.enabled = true;
@@ -23999,10 +24388,7 @@ function createTodoContinuationHook(ctx, config) {
23999
24388
  let hasIncompleteTodos = false;
24000
24389
  let incompleteCount = 0;
24001
24390
  try {
24002
- const todosResult = await ctx.client.session.todo({
24003
- path: { id: sessionID }
24004
- });
24005
- const todos = todosResult.data;
24391
+ const todos = await fetchTodos(sessionID);
24006
24392
  incompleteCount = todos.filter((t) => !TERMINAL_TODO_STATUSES.includes(t.status)).length;
24007
24393
  hasIncompleteTodos = incompleteCount > 0;
24008
24394
  log(`[${HOOK_NAME}] Fetched todos`, {
@@ -24213,10 +24599,7 @@ function createTodoContinuationHook(ctx, config) {
24213
24599
  });
24214
24600
  let hasIncompleteTodos = false;
24215
24601
  try {
24216
- const todosResult = await ctx.client.session.todo({
24217
- path: { id: input.sessionID }
24218
- });
24219
- const todos = todosResult.data;
24602
+ const todos = await fetchTodos(input.sessionID);
24220
24603
  hasIncompleteTodos = todos.some((t) => !TERMINAL_TODO_STATUSES.includes(t.status));
24221
24604
  } catch (error) {
24222
24605
  log(`[${HOOK_NAME}] Warning: failed to fetch todos in command hook`, {
@@ -24240,7 +24623,7 @@ function createTodoContinuationHook(ctx, config) {
24240
24623
  };
24241
24624
  }
24242
24625
  // src/interview/manager.ts
24243
- import path12 from "node:path";
24626
+ import path13 from "node:path";
24244
24627
 
24245
24628
  // src/interview/dashboard.ts
24246
24629
  import crypto from "node:crypto";
@@ -24249,28 +24632,28 @@ import fs7 from "node:fs/promises";
24249
24632
  import {
24250
24633
  createServer
24251
24634
  } from "node:http";
24252
- import os3 from "node:os";
24253
- import path10 from "node:path";
24635
+ import os4 from "node:os";
24636
+ import path11 from "node:path";
24254
24637
  import { URL as URL2 } from "node:url";
24255
24638
 
24256
24639
  // src/interview/document.ts
24257
24640
  import * as fsSync from "node:fs";
24258
24641
  import * as fs6 from "node:fs/promises";
24259
- import * as path9 from "node:path";
24642
+ import * as path10 from "node:path";
24260
24643
  var DEFAULT_OUTPUT_FOLDER = "interview";
24261
24644
  function normalizeOutputFolder(outputFolder) {
24262
24645
  const normalized = outputFolder.trim().replace(/^\/+|\/+$/g, "");
24263
24646
  return normalized || DEFAULT_OUTPUT_FOLDER;
24264
24647
  }
24265
24648
  function createInterviewDirectoryPath(directory, outputFolder) {
24266
- return path9.join(directory, normalizeOutputFolder(outputFolder));
24649
+ return path10.join(directory, normalizeOutputFolder(outputFolder));
24267
24650
  }
24268
24651
  function createInterviewFilePath(directory, outputFolder, idea) {
24269
24652
  const fileName = `${slugify(idea) || "interview"}.md`;
24270
- return path9.join(createInterviewDirectoryPath(directory, outputFolder), fileName);
24653
+ return path10.join(createInterviewDirectoryPath(directory, outputFolder), fileName);
24271
24654
  }
24272
24655
  function relativeInterviewPath(directory, filePath) {
24273
- return path9.relative(directory, filePath) || path9.basename(filePath);
24656
+ return path10.relative(directory, filePath) || path10.basename(filePath);
24274
24657
  }
24275
24658
  function resolveExistingInterviewPath(directory, outputFolder, value) {
24276
24659
  const trimmed = value.trim();
@@ -24279,22 +24662,22 @@ function resolveExistingInterviewPath(directory, outputFolder, value) {
24279
24662
  }
24280
24663
  const outputDir = createInterviewDirectoryPath(directory, outputFolder);
24281
24664
  const candidates = new Set;
24282
- const resolvedRoot = path9.resolve(directory);
24283
- if (path9.isAbsolute(trimmed)) {
24665
+ const resolvedRoot = path10.resolve(directory);
24666
+ if (path10.isAbsolute(trimmed)) {
24284
24667
  candidates.add(trimmed);
24285
24668
  } else {
24286
- candidates.add(path9.resolve(directory, trimmed));
24287
- candidates.add(path9.join(outputDir, trimmed));
24669
+ candidates.add(path10.resolve(directory, trimmed));
24670
+ candidates.add(path10.join(outputDir, trimmed));
24288
24671
  if (!trimmed.endsWith(".md")) {
24289
- candidates.add(path9.join(outputDir, `${trimmed}.md`));
24672
+ candidates.add(path10.join(outputDir, `${trimmed}.md`));
24290
24673
  }
24291
24674
  }
24292
24675
  for (const candidate of candidates) {
24293
- if (path9.extname(candidate) !== ".md") {
24676
+ if (path10.extname(candidate) !== ".md") {
24294
24677
  continue;
24295
24678
  }
24296
- const resolved = path9.resolve(candidate);
24297
- if (!resolved.startsWith(resolvedRoot + path9.sep) && resolved !== resolvedRoot) {
24679
+ const resolved = path10.resolve(candidate);
24680
+ if (!resolved.startsWith(resolvedRoot + path10.sep) && resolved !== resolvedRoot) {
24298
24681
  continue;
24299
24682
  }
24300
24683
  if (fsSync.existsSync(candidate)) {
@@ -24374,7 +24757,7 @@ function parseFrontmatter(content) {
24374
24757
  return result;
24375
24758
  }
24376
24759
  async function ensureInterviewFile(record) {
24377
- await fs6.mkdir(path9.dirname(record.markdownPath), { recursive: true });
24760
+ await fs6.mkdir(path10.dirname(record.markdownPath), { recursive: true });
24378
24761
  try {
24379
24762
  await fs6.access(record.markdownPath);
24380
24763
  } catch {
@@ -26044,12 +26427,12 @@ function renderInterviewPage(interviewId, resumeSlug) {
26044
26427
 
26045
26428
  // src/interview/dashboard.ts
26046
26429
  function getAuthFilePath(port) {
26047
- const dataHome = process.env.XDG_DATA_HOME || path10.join(os3.homedir(), ".local", "share");
26048
- return path10.join(dataHome, "opencode", `.dashboard-${port}.json`);
26430
+ const dataHome = process.env.XDG_DATA_HOME || path11.join(os4.homedir(), ".local", "share");
26431
+ return path11.join(dataHome, "opencode", `.dashboard-${port}.json`);
26049
26432
  }
26050
26433
  function writeAuthFile(port, token) {
26051
26434
  const filePath = getAuthFilePath(port);
26052
- const dir = path10.dirname(filePath);
26435
+ const dir = path11.dirname(filePath);
26053
26436
  try {
26054
26437
  fsSync2.mkdirSync(dir, { recursive: true });
26055
26438
  } catch {}
@@ -26146,7 +26529,7 @@ function createDashboardServer(config) {
26146
26529
  let scanDays = 30;
26147
26530
  function getKnownDirectories() {
26148
26531
  const dirs = new Set;
26149
- dirs.add(os3.homedir());
26532
+ dirs.add(os4.homedir());
26150
26533
  for (const session2 of sessions.values()) {
26151
26534
  if (session2.directory)
26152
26535
  dirs.add(session2.directory);
@@ -26186,7 +26569,7 @@ function createDashboardServer(config) {
26186
26569
  const directories = getKnownDirectories();
26187
26570
  const items = [];
26188
26571
  for (const dir of directories) {
26189
- const interviewDir = path10.join(dir, config.outputFolder);
26572
+ const interviewDir = path11.join(dir, config.outputFolder);
26190
26573
  let entries;
26191
26574
  try {
26192
26575
  entries = await fs7.readdir(interviewDir);
@@ -26198,7 +26581,7 @@ function createDashboardServer(config) {
26198
26581
  continue;
26199
26582
  let content;
26200
26583
  try {
26201
- content = await fs7.readFile(path10.join(interviewDir, entry), "utf8");
26584
+ content = await fs7.readFile(path11.join(interviewDir, entry), "utf8");
26202
26585
  } catch {
26203
26586
  continue;
26204
26587
  }
@@ -26224,7 +26607,7 @@ function createDashboardServer(config) {
26224
26607
  const directories = getKnownDirectories();
26225
26608
  let rebuilt = 0;
26226
26609
  for (const dir of directories) {
26227
- const interviewDir = path10.join(dir, config.outputFolder);
26610
+ const interviewDir = path11.join(dir, config.outputFolder);
26228
26611
  let entries;
26229
26612
  try {
26230
26613
  entries = await fs7.readdir(interviewDir);
@@ -26236,7 +26619,7 @@ function createDashboardServer(config) {
26236
26619
  continue;
26237
26620
  let content;
26238
26621
  try {
26239
- content = await fs7.readFile(path10.join(interviewDir, entry), "utf8");
26622
+ content = await fs7.readFile(path11.join(interviewDir, entry), "utf8");
26240
26623
  } catch {
26241
26624
  continue;
26242
26625
  }
@@ -26262,7 +26645,7 @@ function createDashboardServer(config) {
26262
26645
  questions: [],
26263
26646
  pendingAnswers: null,
26264
26647
  lastUpdatedAt: fm.updatedAt ? new Date(fm.updatedAt).getTime() : Date.now(),
26265
- filePath: path10.join(interviewDir, entry),
26648
+ filePath: path11.join(interviewDir, entry),
26266
26649
  nudgeAction: null
26267
26650
  });
26268
26651
  if (!sessions.has(fm.sessionID)) {
@@ -26526,7 +26909,7 @@ function createDashboardServer(config) {
26526
26909
  const dirs = getKnownDirectories();
26527
26910
  for (const dir of dirs) {
26528
26911
  const slug = extractResumeSlug(interviewId);
26529
- const candidate = path10.join(dir, config.outputFolder, `${slug}.md`);
26912
+ const candidate = path11.join(dir, config.outputFolder, `${slug}.md`);
26530
26913
  try {
26531
26914
  document = await fs7.readFile(candidate, "utf8");
26532
26915
  markdownPath = candidate;
@@ -27052,9 +27435,9 @@ function createInterviewServer(deps) {
27052
27435
  }
27053
27436
 
27054
27437
  // src/interview/service.ts
27055
- import { spawn } from "node:child_process";
27438
+ import { spawn as spawn2 } from "node:child_process";
27056
27439
  import * as fs8 from "node:fs/promises";
27057
- import * as path11 from "node:path";
27440
+ import * as path12 from "node:path";
27058
27441
 
27059
27442
  // src/interview/types.ts
27060
27443
  import { z as z3 } from "zod";
@@ -27256,7 +27639,7 @@ function openBrowser(url) {
27256
27639
  args = [url];
27257
27640
  }
27258
27641
  try {
27259
- const child = spawn(command, args, { detached: true, stdio: "ignore" });
27642
+ const child = spawn2(command, args, { detached: true, stdio: "ignore" });
27260
27643
  child.on("error", (error) => {
27261
27644
  log("[interview] failed to open browser:", { error: error.message, url });
27262
27645
  });
@@ -27321,12 +27704,12 @@ function createInterviewService(ctx, config, deps) {
27321
27704
  if (!newSlug) {
27322
27705
  return;
27323
27706
  }
27324
- const currentFileName = path11.basename(interview.markdownPath, ".md");
27707
+ const currentFileName = path12.basename(interview.markdownPath, ".md");
27325
27708
  if (currentFileName === newSlug) {
27326
27709
  return;
27327
27710
  }
27328
- const dir = path11.dirname(interview.markdownPath);
27329
- const newPath = path11.join(dir, `${newSlug}.md`);
27711
+ const dir = path12.dirname(interview.markdownPath);
27712
+ const newPath = path12.join(dir, `${newSlug}.md`);
27330
27713
  try {
27331
27714
  await fs8.access(newPath);
27332
27715
  return;
@@ -27402,9 +27785,9 @@ function createInterviewService(ctx, config, deps) {
27402
27785
  const messages = await loadMessages(sessionID);
27403
27786
  const title = extractTitle(document);
27404
27787
  const record = {
27405
- id: `${Date.now()}-${++idCounter}-${slugify(path11.basename(markdownPath, ".md")) || "interview"}`,
27788
+ id: `${Date.now()}-${++idCounter}-${slugify(path12.basename(markdownPath, ".md")) || "interview"}`,
27406
27789
  sessionID,
27407
- idea: title || path11.basename(markdownPath, ".md"),
27790
+ idea: title || path12.basename(markdownPath, ".md"),
27408
27791
  markdownPath,
27409
27792
  createdAt: nowIso(),
27410
27793
  status: "active",
@@ -27631,7 +28014,7 @@ function createInterviewService(ctx, config, deps) {
27631
28014
  return fileCache.items;
27632
28015
  }
27633
28016
  const outputDir = createInterviewDirectoryPath(ctx.directory, outputFolder);
27634
- const activePaths = new Set([...interviewsById.values()].filter((i) => i.status === "active").map((i) => path11.resolve(i.markdownPath)));
28017
+ const activePaths = new Set([...interviewsById.values()].filter((i) => i.status === "active").map((i) => path12.resolve(i.markdownPath)));
27635
28018
  let entries;
27636
28019
  try {
27637
28020
  entries = await fs8.readdir(outputDir);
@@ -27642,8 +28025,8 @@ function createInterviewService(ctx, config, deps) {
27642
28025
  for (const entry of entries) {
27643
28026
  if (!entry.endsWith(".md"))
27644
28027
  continue;
27645
- const fullPath = path11.join(outputDir, entry);
27646
- if (activePaths.has(path11.resolve(fullPath)))
28028
+ const fullPath = path12.join(outputDir, entry);
28029
+ if (activePaths.has(path12.resolve(fullPath)))
27647
28030
  continue;
27648
28031
  let content;
27649
28032
  try {
@@ -27742,7 +28125,7 @@ function createInterviewManager(ctx, config) {
27742
28125
  const outputFolder = interviewConfig?.outputFolder ?? "interview";
27743
28126
  if (!dashboardEnabled) {
27744
28127
  const service2 = createInterviewService(ctx, interviewConfig);
27745
- const resolvedOutputPath = path12.join(ctx.directory, outputFolder);
28128
+ const resolvedOutputPath = path13.join(ctx.directory, outputFolder);
27746
28129
  const server = createInterviewServer({
27747
28130
  getState: async (interviewId) => service2.getInterviewState(interviewId),
27748
28131
  listInterviewFiles: async () => service2.listInterviewFiles(),
@@ -27847,7 +28230,7 @@ function createInterviewManager(ctx, config) {
27847
28230
  listInterviews: () => service.listInterviews(),
27848
28231
  submitAnswers: async (interviewId, answers) => service.submitAnswers(interviewId, answers),
27849
28232
  handleNudgeAction: async (interviewId, action) => service.handleNudgeAction(interviewId, action),
27850
- outputFolder: path12.join(ctx.directory, outputFolder),
28233
+ outputFolder: path13.join(ctx.directory, outputFolder),
27851
28234
  port: 0
27852
28235
  });
27853
28236
  service.setBaseUrlResolver(() => perSessionServer.ensureStarted());
@@ -28115,6 +28498,8 @@ function createBuiltinMcps(disabledMcps = [], websearchConfig) {
28115
28498
  }
28116
28499
 
28117
28500
  // src/multiplexer/tmux/index.ts
28501
+ var TMUX_LAYOUT_DEBOUNCE_MS = 150;
28502
+
28118
28503
  class TmuxMultiplexer {
28119
28504
  type = "tmux";
28120
28505
  binaryPath = null;
@@ -28122,6 +28507,8 @@ class TmuxMultiplexer {
28122
28507
  storedLayout;
28123
28508
  storedMainPaneSize;
28124
28509
  targetPane = process.env.TMUX_PANE;
28510
+ layoutTimer;
28511
+ layoutGeneration = 0;
28125
28512
  constructor(layout = "main-vertical", mainPaneSize = 60) {
28126
28513
  this.storedLayout = layout;
28127
28514
  this.storedMainPaneSize = mainPaneSize;
@@ -28183,7 +28570,7 @@ class TmuxMultiplexer {
28183
28570
  if (exitCode === 0 && paneId) {
28184
28571
  const renameProc = crossSpawn([tmux, "select-pane", "-t", paneId, "-T", description.slice(0, 30)], { stdout: "ignore", stderr: "ignore" });
28185
28572
  await renameProc.exited;
28186
- await this.applyLayout(this.storedLayout, this.storedMainPaneSize);
28573
+ this.scheduleLayout();
28187
28574
  log("[tmux] spawnPane: SUCCESS", { paneId });
28188
28575
  return { success: true, paneId };
28189
28576
  }
@@ -28220,7 +28607,7 @@ class TmuxMultiplexer {
28220
28607
  const stderr = await proc.stderr();
28221
28608
  log("[tmux] closePane: result", { exitCode, stderr: stderr.trim() });
28222
28609
  if (exitCode === 0) {
28223
- await this.applyLayout(this.storedLayout, this.storedMainPaneSize);
28610
+ this.scheduleLayout();
28224
28611
  return true;
28225
28612
  }
28226
28613
  log("[tmux] closePane: failed (pane may already be closed)", { paneId });
@@ -28231,41 +28618,74 @@ class TmuxMultiplexer {
28231
28618
  }
28232
28619
  }
28233
28620
  async applyLayout(layout, mainPaneSize) {
28621
+ if (this.layoutTimer) {
28622
+ clearTimeout(this.layoutTimer);
28623
+ this.layoutTimer = undefined;
28624
+ }
28625
+ this.layoutGeneration++;
28626
+ await this.applyLayoutNow(layout, mainPaneSize);
28627
+ }
28628
+ scheduleLayout() {
28629
+ if (this.layoutTimer)
28630
+ clearTimeout(this.layoutTimer);
28631
+ const gen = ++this.layoutGeneration;
28632
+ this.layoutTimer = setTimeout(() => {
28633
+ this.layoutTimer = undefined;
28634
+ if (this.layoutGeneration === gen) {
28635
+ this.applyLayoutNow(this.storedLayout, this.storedMainPaneSize);
28636
+ }
28637
+ }, TMUX_LAYOUT_DEBOUNCE_MS);
28638
+ this.layoutTimer.unref?.();
28639
+ }
28640
+ async applyLayoutNow(layout, mainPaneSize) {
28234
28641
  const tmux = await this.getBinary();
28235
28642
  if (!tmux)
28236
28643
  return;
28237
28644
  this.storedLayout = layout;
28238
28645
  this.storedMainPaneSize = mainPaneSize;
28239
28646
  try {
28240
- const layoutProc = crossSpawn([tmux, "select-layout", ...this.targetArgs(), layout], {
28241
- stdout: "pipe",
28242
- stderr: "pipe"
28243
- });
28244
- await layoutProc.exited;
28647
+ const layoutResult = await this.runTmux(tmux, ["select-layout", ...this.targetArgs(), layout]);
28648
+ if (layoutResult !== 0)
28649
+ return;
28245
28650
  if (layout === "main-horizontal" || layout === "main-vertical") {
28246
28651
  const sizeOption = layout === "main-horizontal" ? "main-pane-height" : "main-pane-width";
28247
- const sizeProc = crossSpawn([
28248
- tmux,
28652
+ const sizeResult = await this.runTmux(tmux, [
28249
28653
  "set-window-option",
28250
28654
  ...this.targetArgs(),
28251
28655
  sizeOption,
28252
28656
  `${mainPaneSize}%`
28253
- ], {
28254
- stdout: "pipe",
28255
- stderr: "pipe"
28256
- });
28257
- await sizeProc.exited;
28258
- const reapplyProc = crossSpawn([tmux, "select-layout", ...this.targetArgs(), layout], {
28259
- stdout: "pipe",
28260
- stderr: "pipe"
28261
- });
28262
- await reapplyProc.exited;
28657
+ ]);
28658
+ if (sizeResult !== 0)
28659
+ return;
28660
+ const reapplyResult = await this.runTmux(tmux, ["select-layout", ...this.targetArgs(), layout]);
28661
+ if (reapplyResult !== 0)
28662
+ return;
28263
28663
  }
28264
28664
  log("[tmux] applyLayout: applied", { layout, mainPaneSize });
28265
28665
  } catch (err) {
28266
28666
  log("[tmux] applyLayout: exception", { error: String(err) });
28267
28667
  }
28268
28668
  }
28669
+ async runTmux(tmux, args) {
28670
+ const proc = crossSpawn([tmux, ...args], {
28671
+ stdout: "pipe",
28672
+ stderr: "pipe"
28673
+ });
28674
+ const [exitCode, , stderr] = await Promise.all([
28675
+ proc.exited,
28676
+ proc.stdout(),
28677
+ proc.stderr()
28678
+ ]);
28679
+ if (exitCode !== 0) {
28680
+ log("[tmux] command failed", {
28681
+ command: args[0],
28682
+ args: [tmux, ...args],
28683
+ exitCode,
28684
+ stderr: stderr.trim()
28685
+ });
28686
+ }
28687
+ return exitCode;
28688
+ }
28269
28689
  async getBinary() {
28270
28690
  await this.isAvailable();
28271
28691
  return this.binaryPath;
@@ -28287,23 +28707,23 @@ class TmuxMultiplexer {
28287
28707
  return null;
28288
28708
  }
28289
28709
  const stdout = await proc.stdout();
28290
- const path13 = stdout.trim().split(`
28710
+ const path14 = stdout.trim().split(`
28291
28711
  `)[0];
28292
- if (!path13) {
28712
+ if (!path14) {
28293
28713
  log("[tmux] findBinary: no path in output");
28294
28714
  return null;
28295
28715
  }
28296
- const verifyProc = crossSpawn([path13, "-V"], {
28716
+ const verifyProc = crossSpawn([path14, "-V"], {
28297
28717
  stdout: "pipe",
28298
28718
  stderr: "pipe"
28299
28719
  });
28300
28720
  const verifyExit = await verifyProc.exited;
28301
28721
  if (verifyExit !== 0) {
28302
- log("[tmux] findBinary: tmux -V failed", { path: path13, verifyExit });
28722
+ log("[tmux] findBinary: tmux -V failed", { path: path14, verifyExit });
28303
28723
  return null;
28304
28724
  }
28305
- log("[tmux] findBinary: found", { path: path13 });
28306
- return path13;
28725
+ log("[tmux] findBinary: found", { path: path14 });
28726
+ return path14;
28307
28727
  } catch (err) {
28308
28728
  log("[tmux] findBinary: exception", { error: String(err) });
28309
28729
  return null;
@@ -29014,17 +29434,17 @@ async function isServerRunning(serverUrl, timeoutMs = 3000, maxAttempts = 2) {
29014
29434
  import { tool as tool2 } from "@opencode-ai/plugin/tool";
29015
29435
 
29016
29436
  // src/tools/ast-grep/cli.ts
29017
- import { existsSync as existsSync8 } from "node:fs";
29437
+ import { existsSync as existsSync9 } from "node:fs";
29018
29438
 
29019
29439
  // src/tools/ast-grep/constants.ts
29020
- import { existsSync as existsSync7, statSync as statSync4 } from "node:fs";
29440
+ import { existsSync as existsSync8, statSync as statSync4 } from "node:fs";
29021
29441
  import { createRequire as createRequire3 } from "node:module";
29022
29442
  import { dirname as dirname6, join as join11 } from "node:path";
29023
29443
 
29024
29444
  // src/tools/ast-grep/downloader.ts
29025
- import { chmodSync, existsSync as existsSync6, mkdirSync as mkdirSync4, unlinkSync as unlinkSync4 } from "node:fs";
29445
+ import { chmodSync, existsSync as existsSync7, mkdirSync as mkdirSync5, unlinkSync as unlinkSync4 } from "node:fs";
29026
29446
  import { createRequire as createRequire2 } from "node:module";
29027
- import { homedir as homedir4 } from "node:os";
29447
+ import { homedir as homedir5 } from "node:os";
29028
29448
  import { join as join10 } from "node:path";
29029
29449
  var REPO = "ast-grep/ast-grep";
29030
29450
  var DEFAULT_VERSION = "0.40.0";
@@ -29049,11 +29469,11 @@ var PLATFORM_MAP = {
29049
29469
  function getCacheDir2() {
29050
29470
  if (process.platform === "win32") {
29051
29471
  const localAppData = process.env.LOCALAPPDATA || process.env.APPDATA;
29052
- const base2 = localAppData || join10(homedir4(), "AppData", "Local");
29472
+ const base2 = localAppData || join10(homedir5(), "AppData", "Local");
29053
29473
  return join10(base2, "oh-my-opencode-slim", "bin");
29054
29474
  }
29055
29475
  const xdgCache = process.env.XDG_CACHE_HOME;
29056
- const base = xdgCache || join10(homedir4(), ".cache");
29476
+ const base = xdgCache || join10(homedir5(), ".cache");
29057
29477
  return join10(base, "oh-my-opencode-slim", "bin");
29058
29478
  }
29059
29479
  function getBinaryName() {
@@ -29061,7 +29481,7 @@ function getBinaryName() {
29061
29481
  }
29062
29482
  function getCachedBinaryPath() {
29063
29483
  const binaryPath = join10(getCacheDir2(), getBinaryName());
29064
- return existsSync6(binaryPath) ? binaryPath : null;
29484
+ return existsSync7(binaryPath) ? binaryPath : null;
29065
29485
  }
29066
29486
  async function downloadAstGrep(version = DEFAULT_VERSION) {
29067
29487
  const platformKey = `${process.platform}-${process.arch}`;
@@ -29073,16 +29493,16 @@ async function downloadAstGrep(version = DEFAULT_VERSION) {
29073
29493
  const cacheDir = getCacheDir2();
29074
29494
  const binaryName = getBinaryName();
29075
29495
  const binaryPath = join10(cacheDir, binaryName);
29076
- if (existsSync6(binaryPath)) {
29496
+ if (existsSync7(binaryPath)) {
29077
29497
  return binaryPath;
29078
29498
  }
29079
- const { arch, os: os4 } = platformInfo;
29080
- const assetName = `app-${arch}-${os4}.zip`;
29499
+ const { arch, os: os5 } = platformInfo;
29500
+ const assetName = `app-${arch}-${os5}.zip`;
29081
29501
  const downloadUrl = `https://github.com/${REPO}/releases/download/${version}/${assetName}`;
29082
29502
  console.log(`[oh-my-opencode-slim] Downloading ast-grep binary...`);
29083
29503
  try {
29084
- if (!existsSync6(cacheDir)) {
29085
- mkdirSync4(cacheDir, { recursive: true });
29504
+ if (!existsSync7(cacheDir)) {
29505
+ mkdirSync5(cacheDir, { recursive: true });
29086
29506
  }
29087
29507
  const response = await fetch(downloadUrl, { redirect: "follow" });
29088
29508
  if (!response.ok) {
@@ -29092,10 +29512,10 @@ async function downloadAstGrep(version = DEFAULT_VERSION) {
29092
29512
  const arrayBuffer = await response.arrayBuffer();
29093
29513
  await crossWrite(archivePath, arrayBuffer);
29094
29514
  await extractZip(archivePath, cacheDir);
29095
- if (existsSync6(archivePath)) {
29515
+ if (existsSync7(archivePath)) {
29096
29516
  unlinkSync4(archivePath);
29097
29517
  }
29098
- if (process.platform !== "win32" && existsSync6(binaryPath)) {
29518
+ if (process.platform !== "win32" && existsSync7(binaryPath)) {
29099
29519
  chmodSync(binaryPath, 493);
29100
29520
  }
29101
29521
  console.log(`[oh-my-opencode-slim] ast-grep binary ready.`);
@@ -29178,7 +29598,7 @@ function findSgCliPathSync() {
29178
29598
  const cliPkgPath = require2.resolve("@ast-grep/cli/package.json");
29179
29599
  const cliDir = dirname6(cliPkgPath);
29180
29600
  const sgPath = join11(cliDir, binaryName);
29181
- if (existsSync7(sgPath) && isValidBinary(sgPath)) {
29601
+ if (existsSync8(sgPath) && isValidBinary(sgPath)) {
29182
29602
  return sgPath;
29183
29603
  }
29184
29604
  } catch {}
@@ -29190,16 +29610,16 @@ function findSgCliPathSync() {
29190
29610
  const pkgDir = dirname6(pkgPath);
29191
29611
  const astGrepName = process.platform === "win32" ? "ast-grep.exe" : "ast-grep";
29192
29612
  const binaryPath = join11(pkgDir, astGrepName);
29193
- if (existsSync7(binaryPath) && isValidBinary(binaryPath)) {
29613
+ if (existsSync8(binaryPath) && isValidBinary(binaryPath)) {
29194
29614
  return binaryPath;
29195
29615
  }
29196
29616
  } catch {}
29197
29617
  }
29198
29618
  if (process.platform === "darwin") {
29199
29619
  const homebrewPaths = ["/opt/homebrew/bin/sg", "/usr/local/bin/sg"];
29200
- for (const path13 of homebrewPaths) {
29201
- if (existsSync7(path13) && isValidBinary(path13)) {
29202
- return path13;
29620
+ for (const path14 of homebrewPaths) {
29621
+ if (existsSync8(path14) && isValidBinary(path14)) {
29622
+ return path14;
29203
29623
  }
29204
29624
  }
29205
29625
  }
@@ -29216,8 +29636,8 @@ function getSgCliPath() {
29216
29636
  }
29217
29637
  return "sg";
29218
29638
  }
29219
- function setSgCliPath(path13) {
29220
- resolvedCliPath = path13;
29639
+ function setSgCliPath(path14) {
29640
+ resolvedCliPath = path14;
29221
29641
  }
29222
29642
  var DEFAULT_TIMEOUT_MS2 = 300000;
29223
29643
  var DEFAULT_MAX_OUTPUT_BYTES = 1 * 1024 * 1024;
@@ -29227,7 +29647,7 @@ var DEFAULT_MAX_MATCHES = 500;
29227
29647
  var initPromise = null;
29228
29648
  async function getAstGrepPath() {
29229
29649
  const currentPath = getSgCliPath();
29230
- if (currentPath !== "sg" && existsSync8(currentPath)) {
29650
+ if (currentPath !== "sg" && existsSync9(currentPath)) {
29231
29651
  return currentPath;
29232
29652
  }
29233
29653
  if (initPromise) {
@@ -29235,7 +29655,7 @@ async function getAstGrepPath() {
29235
29655
  }
29236
29656
  initPromise = (async () => {
29237
29657
  const syncPath = findSgCliPathSync();
29238
- if (syncPath && existsSync8(syncPath)) {
29658
+ if (syncPath && existsSync9(syncPath)) {
29239
29659
  setSgCliPath(syncPath);
29240
29660
  return syncPath;
29241
29661
  }
@@ -29274,7 +29694,7 @@ async function runSg(options) {
29274
29694
  const paths2 = options.paths && options.paths.length > 0 ? options.paths : ["."];
29275
29695
  args.push(...paths2);
29276
29696
  let cliPath = getSgCliPath();
29277
- if (!existsSync8(cliPath) && cliPath !== "sg") {
29697
+ if (!existsSync9(cliPath) && cliPath !== "sg") {
29278
29698
  const downloadedPath = await getAstGrepPath();
29279
29699
  if (downloadedPath) {
29280
29700
  cliPath = downloadedPath;
@@ -29622,6 +30042,74 @@ Returns the councillor responses with a summary footer.`,
29622
30042
  });
29623
30043
  return { council_session };
29624
30044
  }
30045
+ // src/tui-state.ts
30046
+ import * as fs9 from "node:fs";
30047
+ import * as os5 from "node:os";
30048
+ import * as path14 from "node:path";
30049
+ var STATE_DIR = "oh-my-opencode-slim";
30050
+ var STATE_FILE = "tui-state.json";
30051
+ function dataDir() {
30052
+ return process.env.XDG_DATA_HOME ?? path14.join(os5.homedir(), ".local", "share");
30053
+ }
30054
+ function getTuiStatePath() {
30055
+ return path14.join(dataDir(), "opencode", "storage", STATE_DIR, STATE_FILE);
30056
+ }
30057
+ function emptySnapshot() {
30058
+ return {
30059
+ version: 1,
30060
+ updatedAt: Date.now(),
30061
+ agentModels: {}
30062
+ };
30063
+ }
30064
+ function parseSnapshot(value) {
30065
+ const parsed = JSON.parse(value);
30066
+ if (parsed?.version !== 1)
30067
+ return emptySnapshot();
30068
+ return {
30069
+ version: 1,
30070
+ updatedAt: typeof parsed.updatedAt === "number" ? parsed.updatedAt : Date.now(),
30071
+ agentModels: parsed.agentModels ?? {}
30072
+ };
30073
+ }
30074
+ function readTuiSnapshot() {
30075
+ try {
30076
+ return parseSnapshot(fs9.readFileSync(getTuiStatePath(), "utf8"));
30077
+ } catch {
30078
+ return emptySnapshot();
30079
+ }
30080
+ }
30081
+ async function readTuiSnapshotAsync() {
30082
+ try {
30083
+ return parseSnapshot(await fs9.promises.readFile(getTuiStatePath(), "utf8"));
30084
+ } catch {
30085
+ return emptySnapshot();
30086
+ }
30087
+ }
30088
+ function writeTuiSnapshot(snapshot) {
30089
+ try {
30090
+ const filePath = getTuiStatePath();
30091
+ fs9.mkdirSync(path14.dirname(filePath), { recursive: true });
30092
+ fs9.writeFileSync(filePath, `${JSON.stringify(snapshot)}
30093
+ `);
30094
+ } catch {}
30095
+ }
30096
+ function updateSnapshot(mutator) {
30097
+ const snapshot = readTuiSnapshot();
30098
+ mutator(snapshot);
30099
+ snapshot.updatedAt = Date.now();
30100
+ writeTuiSnapshot(snapshot);
30101
+ }
30102
+ function recordTuiAgentModels(input) {
30103
+ updateSnapshot((snapshot) => {
30104
+ snapshot.agentModels = { ...input.agentModels };
30105
+ });
30106
+ }
30107
+ function recordTuiAgentModel(input) {
30108
+ updateSnapshot((snapshot) => {
30109
+ snapshot.agentModels[input.agentName] = input.model;
30110
+ });
30111
+ }
30112
+
29625
30113
  // src/tools/preset-manager.ts
29626
30114
  var COMMAND_NAME3 = "preset";
29627
30115
  function createPresetManager(ctx, config) {
@@ -29698,6 +30186,14 @@ function createPresetManager(ctx, config) {
29698
30186
  await ctx.client.config.update({
29699
30187
  body: { agent: allUpdates }
29700
30188
  });
30189
+ const snapshot = readTuiSnapshot();
30190
+ const agentModels = { ...snapshot.agentModels };
30191
+ for (const [agentName, agentConfig] of Object.entries(allUpdates)) {
30192
+ if (typeof agentConfig.model === "string") {
30193
+ agentModels[agentName] = agentConfig.model;
30194
+ }
30195
+ }
30196
+ recordTuiAgentModels({ agentModels });
29701
30197
  activePreset = presetName;
29702
30198
  const summaryParts = [];
29703
30199
  for (const [name, cfg] of Object.entries(agentUpdates)) {
@@ -29808,15 +30304,15 @@ var BINARY_PREFIXES = [
29808
30304
  ];
29809
30305
  var WEBFETCH_DESCRIPTION = "Fetch a URL with better extraction for static/docs pages. Supports llms.txt probing, content-focused HTML extraction, metadata, redirects, and an optional prompt processed by a cheap secondary model.";
29810
30306
  // src/tools/smartfetch/tool.ts
29811
- import os4 from "node:os";
29812
- import path16 from "node:path";
30307
+ import os6 from "node:os";
30308
+ import path18 from "node:path";
29813
30309
  import {
29814
30310
  tool as tool4
29815
30311
  } from "@opencode-ai/plugin";
29816
30312
 
29817
30313
  // src/tools/smartfetch/binary.ts
29818
30314
  import { mkdir as mkdir2, writeFile as writeFile2 } from "node:fs/promises";
29819
- import path13 from "node:path";
30315
+ import path15 from "node:path";
29820
30316
  function extensionForMime(contentType) {
29821
30317
  const mime = contentType.split(";")[0]?.trim().toLowerCase();
29822
30318
  const map = {
@@ -29837,10 +30333,10 @@ function buildBinaryResultMessage(fetchResult, savedPath) {
29837
30333
  async function saveBinary(binaryDir, data, contentType, filename) {
29838
30334
  await mkdir2(binaryDir, { recursive: true });
29839
30335
  const initialName = filename || `webfetch-${Date.now()}.${extensionForMime(contentType)}`;
29840
- const parsed = path13.parse(initialName);
30336
+ const parsed = path15.parse(initialName);
29841
30337
  for (let attempt = 0;attempt < 1000; attempt++) {
29842
30338
  const candidateName = attempt === 0 ? initialName : `${parsed.name}-${attempt}${parsed.ext || `.${extensionForMime(contentType)}`}`;
29843
- const file = path13.join(binaryDir, candidateName);
30339
+ const file = path15.join(binaryDir, candidateName);
29844
30340
  try {
29845
30341
  await writeFile2(file, data, { flag: "wx" });
29846
30342
  return file;
@@ -30494,7 +30990,7 @@ var L = class u2 {
30494
30990
  };
30495
30991
 
30496
30992
  // src/tools/smartfetch/network.ts
30497
- import path14 from "node:path";
30993
+ import path16 from "node:path";
30498
30994
 
30499
30995
  // src/tools/smartfetch/utils.ts
30500
30996
  var import_readability = __toESM(require_readability(), 1);
@@ -31219,7 +31715,7 @@ function inferFilenameFromUrl(url) {
31219
31715
  function truncateFilename(name, maxLength = 180) {
31220
31716
  if (name.length <= maxLength)
31221
31717
  return name;
31222
- const parsed = path14.parse(name);
31718
+ const parsed = path16.parse(name);
31223
31719
  const ext = parsed.ext || "";
31224
31720
  const baseLimit = Math.max(1, maxLength - ext.length);
31225
31721
  return `${parsed.name.slice(0, baseLimit)}${ext}`;
@@ -31389,9 +31885,9 @@ function isInvalidLlmsResult(fetchResult) {
31389
31885
  }
31390
31886
 
31391
31887
  // src/tools/smartfetch/secondary-model.ts
31392
- import { existsSync as existsSync9 } from "node:fs";
31888
+ import { existsSync as existsSync10 } from "node:fs";
31393
31889
  import { readFile as readFile4 } from "node:fs/promises";
31394
- import path15 from "node:path";
31890
+ import path17 from "node:path";
31395
31891
  function parseModelRef(value) {
31396
31892
  if (!value)
31397
31893
  return;
@@ -31417,8 +31913,8 @@ function pickAgentModelRef(value) {
31417
31913
  }
31418
31914
  function findPreferredOpenCodeConfigPath(baseDir) {
31419
31915
  for (const file of ["opencode.jsonc", "opencode.json"]) {
31420
- const fullPath = path15.join(baseDir, file);
31421
- if (existsSync9(fullPath))
31916
+ const fullPath = path17.join(baseDir, file);
31917
+ if (existsSync10(fullPath))
31422
31918
  return fullPath;
31423
31919
  }
31424
31920
  return;
@@ -31434,7 +31930,7 @@ async function readOpenCodeConfigFile(configPath) {
31434
31930
  }
31435
31931
  }
31436
31932
  async function readEffectiveOpenCodeConfig(directory) {
31437
- const projectDir = path15.join(directory, ".opencode");
31933
+ const projectDir = path17.join(directory, ".opencode");
31438
31934
  const userDirs = getConfigSearchDirs();
31439
31935
  const projectPath = findPreferredOpenCodeConfigPath(projectDir);
31440
31936
  const userPath = userDirs.map((configDir) => findPreferredOpenCodeConfigPath(configDir)).find(Boolean);
@@ -31595,7 +32091,7 @@ async function runSecondaryModelWithFallback(client, directory, models, prompt,
31595
32091
  // src/tools/smartfetch/tool.ts
31596
32092
  var z5 = tool4.schema;
31597
32093
  function createWebfetchTool(pluginCtx, options = {}) {
31598
- const binaryDir = options.binaryDir || path16.join(os4.tmpdir(), "opencode-smartfetch");
32094
+ const binaryDir = options.binaryDir || path18.join(os6.tmpdir(), "opencode-smartfetch");
31599
32095
  return tool4({
31600
32096
  description: WEBFETCH_DESCRIPTION,
31601
32097
  args: {
@@ -32191,6 +32687,7 @@ var OhMyOpenCodeLite = async (ctx) => {
32191
32687
  let taskSessionManagerHook;
32192
32688
  let interviewManager;
32193
32689
  let presetManager;
32690
+ let divoomManager;
32194
32691
  let councilTools;
32195
32692
  let webfetch;
32196
32693
  let rewriteDisplayNameMentions;
@@ -32258,7 +32755,6 @@ var OhMyOpenCodeLite = async (ctx) => {
32258
32755
  webfetch = createWebfetchTool(ctx);
32259
32756
  multiplexerSessionManager = new MultiplexerSessionManager(ctx, multiplexerConfig);
32260
32757
  autoUpdateChecker = createAutoUpdateCheckerHook(ctx, {
32261
- showStartupToast: config.showStartupToast ?? true,
32262
32758
  autoUpdate: config.autoUpdate ?? true
32263
32759
  });
32264
32760
  phaseReminderHook = createPhaseReminderHook();
@@ -32286,6 +32782,7 @@ var OhMyOpenCodeLite = async (ctx) => {
32286
32782
  });
32287
32783
  interviewManager = createInterviewManager(ctx, config);
32288
32784
  presetManager = createPresetManager(ctx, config);
32785
+ divoomManager = createDivoomManager(config.divoom);
32289
32786
  toolCount = Object.keys(councilTools).length + Object.keys(todoContinuationHook.tool).length + 1 + 2;
32290
32787
  } catch (err) {
32291
32788
  log("[plugin] FATAL: init failed", String(err));
@@ -32322,6 +32819,7 @@ var OhMyOpenCodeLite = async (ctx) => {
32322
32819
  appLog(ctx, "warn", msg).catch(() => {});
32323
32820
  }
32324
32821
  });
32822
+ divoomManager.onPluginLoad();
32325
32823
  return {
32326
32824
  name: "oh-my-opencode-slim",
32327
32825
  agent: agents,
@@ -32478,6 +32976,15 @@ var OhMyOpenCodeLite = async (ctx) => {
32478
32976
  }
32479
32977
  }
32480
32978
  }
32979
+ const tuiAgentModels = {};
32980
+ for (const agentDef of agentDefs) {
32981
+ if (agentDef.name === "councillor")
32982
+ continue;
32983
+ const entry = configAgent[agentDef.name];
32984
+ const resolvedModel = typeof entry?.model === "string" ? entry.model : runtimeChains[agentDef.name]?.[0] ? runtimeChains[agentDef.name][0] : typeof agentDef.config.model === "string" ? agentDef.config.model : undefined;
32985
+ tuiAgentModels[agentDef.name] = resolvedModel ?? "default";
32986
+ }
32987
+ recordTuiAgentModels({ agentModels: tuiAgentModels });
32481
32988
  const configMcp = opencodeConfig.mcp;
32482
32989
  if (!configMcp) {
32483
32990
  opencodeConfig.mcp = { ...mcps };
@@ -32521,6 +33028,15 @@ var OhMyOpenCodeLite = async (ctx) => {
32521
33028
  },
32522
33029
  event: async (input) => {
32523
33030
  const event = input.event;
33031
+ if (event.type === "message.updated") {
33032
+ const info = event.properties?.info;
33033
+ if (typeof info?.agent === "string" && typeof info.providerID === "string" && typeof info.modelID === "string") {
33034
+ recordTuiAgentModel({
33035
+ agentName: resolveRuntimeAgentName(config, info.agent),
33036
+ model: `${info.providerID}/${info.modelID}`
33037
+ });
33038
+ }
33039
+ }
32524
33040
  if (event.type === "session.created") {
32525
33041
  const childSessionId = event.properties?.info?.id;
32526
33042
  const parentSessionId = event.properties?.info?.parentID;
@@ -32536,6 +33052,37 @@ var OhMyOpenCodeLite = async (ctx) => {
32536
33052
  await multiplexerSessionManager.onSessionDeleted(event);
32537
33053
  await interviewManager.handleEvent(input);
32538
33054
  await taskSessionManagerHook.event(input);
33055
+ if (event.type === "permission.asked" || event.type === "question.asked") {
33056
+ const props = event.properties;
33057
+ divoomManager.onUserInputRequired({
33058
+ sessionId: props?.sessionID,
33059
+ requestId: props?.id ?? props?.requestID
33060
+ });
33061
+ }
33062
+ if (event.type === "permission.replied" || event.type === "question.replied" || event.type === "question.rejected") {
33063
+ const props = event.properties;
33064
+ divoomManager.onUserInputResolved({
33065
+ sessionId: props?.sessionID,
33066
+ requestId: props?.requestID ?? props?.id
33067
+ });
33068
+ }
33069
+ if (input.event.type === "session.status") {
33070
+ const props = input.event.properties;
33071
+ const sessionID = props?.sessionID;
33072
+ divoomManager.onOrchestratorStatus({
33073
+ sessionId: sessionID,
33074
+ status: props?.status?.type,
33075
+ isOrchestrator: sessionID ? sessionAgentMap.get(sessionID) === "orchestrator" : false
33076
+ });
33077
+ }
33078
+ if (input.event.type === "session.deleted") {
33079
+ const props = input.event.properties;
33080
+ const sessionID = props?.info?.id ?? props?.sessionID;
33081
+ divoomManager.onSessionDeleted({
33082
+ sessionId: sessionID,
33083
+ isOrchestrator: sessionID ? sessionAgentMap.get(sessionID) === "orchestrator" : false
33084
+ });
33085
+ }
32539
33086
  if (input.event.type === "session.deleted") {
32540
33087
  const props = input.event.properties;
32541
33088
  const sessionID = props?.info?.id ?? props?.sessionID;
@@ -32550,6 +33097,13 @@ var OhMyOpenCodeLite = async (ctx) => {
32550
33097
  "tool.execute.before": async (input, output) => {
32551
33098
  await applyPatchHook["tool.execute.before"](input, output);
32552
33099
  await taskSessionManagerHook["tool.execute.before"](input, output);
33100
+ if (input.tool.toLowerCase() === "task") {
33101
+ divoomManager.onTaskStart({
33102
+ parentSessionId: input.sessionID,
33103
+ callId: input.callID,
33104
+ args: output.args
33105
+ });
33106
+ }
32553
33107
  },
32554
33108
  "command.execute.before": async (input, output) => {
32555
33109
  await todoContinuationHook.handleCommandExecuteBefore(input, output);
@@ -32612,11 +33166,31 @@ ${output.system[0]}` : "");
32612
33166
  await filterAvailableSkillsHook["experimental.chat.messages.transform"](input, typedOutput);
32613
33167
  },
32614
33168
  "tool.execute.after": async (input, output) => {
32615
- await delegateTaskRetryHook["tool.execute.after"](input, output);
32616
- await jsonErrorRecoveryHook["tool.execute.after"](input, output);
32617
- await todoContinuationHook.handleToolExecuteAfter(input, output);
32618
- await postFileToolNudgeHook["tool.execute.after"](input, output);
32619
- await taskSessionManagerHook["tool.execute.after"](input, output);
33169
+ const meta = input;
33170
+ const runPostToolHook = async (name, fn) => {
33171
+ try {
33172
+ await fn();
33173
+ } catch (error) {
33174
+ log("[plugin] post-tool hook failed open", {
33175
+ hook: name,
33176
+ tool: meta.tool,
33177
+ sessionID: meta.sessionID,
33178
+ callID: meta.callID,
33179
+ error: error instanceof Error ? error.message : String(error)
33180
+ });
33181
+ }
33182
+ };
33183
+ await runPostToolHook("delegate-task-retry", () => delegateTaskRetryHook["tool.execute.after"](input, output));
33184
+ await runPostToolHook("json-error-recovery", () => jsonErrorRecoveryHook["tool.execute.after"](input, output));
33185
+ await runPostToolHook("todo-continuation", () => todoContinuationHook.handleToolExecuteAfter(input, output));
33186
+ await runPostToolHook("post-file-tool-nudge", () => postFileToolNudgeHook["tool.execute.after"](input, output));
33187
+ await runPostToolHook("task-session-manager", () => taskSessionManagerHook["tool.execute.after"](input, output));
33188
+ if (input.tool.toLowerCase() === "task") {
33189
+ divoomManager.onTaskEnd({
33190
+ parentSessionId: input.sessionID,
33191
+ callId: input.callID
33192
+ });
33193
+ }
32620
33194
  }
32621
33195
  };
32622
33196
  };