oh-my-opencode-slim 1.0.6 → 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(path15) {
6250
- if (!path15)
6251
- return path15;
6249
+ function remove_dot_segments(path16) {
6250
+ if (!path16)
6251
+ return path16;
6252
6252
  var output = "";
6253
- while (path15.length > 0) {
6254
- if (path15 === "." || path15 === "..") {
6255
- path15 = "";
6253
+ while (path16.length > 0) {
6254
+ if (path16 === "." || path16 === "..") {
6255
+ path16 = "";
6256
6256
  break;
6257
6257
  }
6258
- var twochars = path15.substring(0, 2);
6259
- var threechars = path15.substring(0, 3);
6260
- var fourchars = path15.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
- path15 = path15.substring(3);
6262
+ path16 = path16.substring(3);
6263
6263
  } else if (twochars === "./") {
6264
- path15 = path15.substring(2);
6264
+ path16 = path16.substring(2);
6265
6265
  } else if (threechars === "/./") {
6266
- path15 = "/" + path15.substring(3);
6267
- } else if (twochars === "/." && path15.length === 2) {
6268
- path15 = "/";
6269
- } else if (fourchars === "/../" || threechars === "/.." && path15.length === 3) {
6270
- path15 = "/" + path15.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 = path15.match(/(\/?([^\/]*))/)[0];
6273
+ var segment = path16.match(/(\/?([^\/]*))/)[0];
6274
6274
  output += segment;
6275
- path15 = path15.substring(segment.length);
6275
+ path16 = path16.substring(segment.length);
6276
6276
  }
6277
6277
  }
6278
6278
  return output;
@@ -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)"),
@@ -18581,6 +18592,7 @@ var PluginConfigSchema = z2.object({
18581
18592
  websearch: WebsearchConfigSchema.optional(),
18582
18593
  interview: InterviewConfigSchema.optional(),
18583
18594
  sessionManager: SessionManagerConfigSchema.optional(),
18595
+ divoom: DivoomConfigSchema.optional(),
18584
18596
  todoContinuation: TodoContinuationConfigSchema.optional(),
18585
18597
  fallback: FailoverConfigSchema.optional(),
18586
18598
  council: CouncilConfigSchema.optional()
@@ -18597,20 +18609,49 @@ var PluginConfigSchema = z2.object({
18597
18609
 
18598
18610
  // src/config/loader.ts
18599
18611
  var PROMPTS_DIR_NAME = "oh-my-opencode-slim";
18600
- function loadConfigFromPath(configPath) {
18612
+ function loadConfigFromPath(configPath, options) {
18601
18613
  try {
18602
18614
  const content = fs.readFileSync(configPath, "utf-8");
18603
- 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
+ }
18604
18630
  const result = PluginConfigSchema.safeParse(rawConfig);
18605
18631
  if (!result.success) {
18606
- console.warn(`[oh-my-opencode-slim] Invalid config at ${configPath}:`);
18607
- 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
+ }
18608
18642
  return null;
18609
18643
  }
18610
18644
  return result.data;
18611
18645
  } catch (error) {
18612
18646
  if (error instanceof Error && "code" in error && error.code !== "ENOENT") {
18613
- 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
+ }
18614
18655
  }
18615
18656
  return null;
18616
18657
  }
@@ -18635,6 +18676,26 @@ function findConfigPathInDirs(configDirs, baseName) {
18635
18676
  }
18636
18677
  return null;
18637
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
+ }
18638
18699
  function deepMerge(base, override) {
18639
18700
  if (!base)
18640
18701
  return override;
@@ -18652,24 +18713,12 @@ function deepMerge(base, override) {
18652
18713
  }
18653
18714
  return result;
18654
18715
  }
18655
- function loadPluginConfig(directory) {
18656
- const userConfigPath = findConfigPathInDirs(getConfigSearchDirs(), "oh-my-opencode-slim");
18657
- const projectConfigBasePath = path.join(directory, ".opencode", "oh-my-opencode-slim");
18658
- const projectConfigPath = findConfigPath(projectConfigBasePath);
18659
- let config = userConfigPath ? loadConfigFromPath(userConfigPath) ?? {} : {};
18660
- 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;
18661
18720
  if (projectConfig) {
18662
- config = {
18663
- ...config,
18664
- ...projectConfig,
18665
- agents: deepMerge(config.agents, projectConfig.agents),
18666
- tmux: deepMerge(config.tmux, projectConfig.tmux),
18667
- multiplexer: deepMerge(config.multiplexer, projectConfig.multiplexer),
18668
- interview: deepMerge(config.interview, projectConfig.interview),
18669
- sessionManager: deepMerge(config.sessionManager, projectConfig.sessionManager),
18670
- fallback: deepMerge(config.fallback, projectConfig.fallback),
18671
- council: deepMerge(config.council, projectConfig.council)
18672
- };
18721
+ config = mergePluginConfigs(config, projectConfig);
18673
18722
  }
18674
18723
  config = migrateTmuxToMultiplexer(config);
18675
18724
  const envPreset = process.env.OH_MY_OPENCODE_SLIM_PRESET;
@@ -18683,7 +18732,15 @@ function loadPluginConfig(directory) {
18683
18732
  } else {
18684
18733
  const presetSource = envPreset === config.preset ? "environment variable" : "config file";
18685
18734
  const availablePresets = config.presets ? Object.keys(config.presets).join(", ") : "none";
18686
- 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
+ }
18687
18744
  }
18688
18745
  }
18689
18746
  return config;
@@ -18744,6 +18801,34 @@ function getCustomAgentNames(config) {
18744
18801
  });
18745
18802
  }
18746
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
+ }
18747
18832
  function shortModelLabel(model) {
18748
18833
  return model.split("/").pop() ?? model;
18749
18834
  }
@@ -18771,11 +18856,17 @@ async function promptWithTimeout(client, args, timeoutMs) {
18771
18856
  promptPromise,
18772
18857
  new Promise((_, reject) => {
18773
18858
  timer = setTimeout(() => {
18774
- client.session.abort({ path: { id: sessionId } }).catch(() => {});
18775
- reject(new Error(`Prompt timed out after ${timeoutMs}ms`));
18859
+ reject(new OperationTimeoutError(`Prompt timed out after ${timeoutMs}ms`));
18776
18860
  }, timeoutMs);
18777
18861
  })
18778
18862
  ]);
18863
+ } catch (error) {
18864
+ if (error instanceof OperationTimeoutError) {
18865
+ try {
18866
+ await abortSessionWithTimeout(client, sessionId);
18867
+ } catch {}
18868
+ }
18869
+ throw error;
18779
18870
  } finally {
18780
18871
  clearTimeout(timer);
18781
18872
  }
@@ -18822,7 +18913,7 @@ var AGENT_DESCRIPTIONS = {
18822
18913
  - **Don't delegate when:** Know the path and need actual content • Need full file anyway • Single specific lookup • About to edit the file`,
18823
18914
  librarian: `@librarian
18824
18915
  - Role: Authoritative source for current library docs and API references
18825
- - Permissions: None
18916
+ - Permissions: External docs/search MCPs; no file edits
18826
18917
  - Stats: 10x better finding up-to-date library docs than orchestrator, 1/2 cost of orchestrator
18827
18918
  - Capabilities: Fetches latest official docs, examples, API signatures, version-specific behavior via grep_app MCP
18828
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
@@ -19207,7 +19298,8 @@ function createCouncillorAgent(model, customPrompt, customAppendPrompt) {
19207
19298
  grep: "allow",
19208
19299
  lsp: "allow",
19209
19300
  list: "allow",
19210
- codesearch: "allow"
19301
+ codesearch: "allow",
19302
+ ast_grep_search: "allow"
19211
19303
  }
19212
19304
  }
19213
19305
  };
@@ -19529,10 +19621,14 @@ ${customAppendPrompt}`;
19529
19621
 
19530
19622
  // src/agents/index.ts
19531
19623
  var COUNCIL_TOOL_ALLOWED_AGENTS = new Set(["council"]);
19624
+ var SAFE_AGENT_ALIAS_RE = /^[a-z][a-z0-9_-]*$/i;
19532
19625
  function normalizeDisplayName(displayName) {
19533
19626
  const trimmed = displayName.trim();
19534
19627
  return trimmed.startsWith("@") ? trimmed.slice(1) : trimmed;
19535
19628
  }
19629
+ function isSafeDisplayName(displayName) {
19630
+ return SAFE_AGENT_ALIAS_RE.test(displayName);
19631
+ }
19536
19632
  function escapeRegExp(value) {
19537
19633
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
19538
19634
  }
@@ -19566,7 +19662,7 @@ function normalizeCustomAgentName(name) {
19566
19662
  return name.trim();
19567
19663
  }
19568
19664
  function isSafeCustomAgentName(name) {
19569
- return /^[a-z][a-z0-9_-]*$/i.test(name) && !isKnownAgentName(name);
19665
+ return SAFE_AGENT_ALIAS_RE.test(name) && !isKnownAgentName(name);
19570
19666
  }
19571
19667
  function hasCustomAgentModel(override) {
19572
19668
  if (!override?.model) {
@@ -19626,6 +19722,9 @@ var SUBAGENT_FACTORIES = {
19626
19722
  };
19627
19723
  function createAgents(config) {
19628
19724
  const disabled = getDisabledAgents(config);
19725
+ if (!config?.council) {
19726
+ disabled.add("council");
19727
+ }
19629
19728
  const getModelForAgent = (name) => {
19630
19729
  if (name === "fixer" && !getAgentOverride(config, "fixer")?.model) {
19631
19730
  const librarianOverride = getAgentOverride(config, "librarian")?.model;
@@ -19712,6 +19811,9 @@ function createAgents(config) {
19712
19811
  const usedDisplayNames = new Set;
19713
19812
  for (const [, displayName] of displayNameMap) {
19714
19813
  const normalizedDisplayName = normalizeDisplayName(displayName);
19814
+ if (!isSafeDisplayName(normalizedDisplayName)) {
19815
+ throw new Error(`displayName '${normalizedDisplayName}' must match /^[a-z][a-z0-9_-]*$/i`);
19816
+ }
19715
19817
  if (usedDisplayNames.has(normalizedDisplayName)) {
19716
19818
  throw new Error(`Duplicate displayName '${normalizedDisplayName}' assigned to multiple agents`);
19717
19819
  }
@@ -20138,6 +20240,286 @@ class CouncilManager {
20138
20240
  };
20139
20241
  }
20140
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
+
20141
20523
  // src/hooks/apply-patch/errors.ts
20142
20524
  var APPLY_PATCH_ERROR_PREFIX = {
20143
20525
  blocked: "apply_patch blocked",
@@ -20206,7 +20588,7 @@ function ensureApplyPatchError(error, context) {
20206
20588
 
20207
20589
  // src/hooks/apply-patch/execution-context.ts
20208
20590
  import * as fs3 from "node:fs/promises";
20209
- import path3 from "node:path";
20591
+ import path4 from "node:path";
20210
20592
 
20211
20593
  // src/hooks/apply-patch/codec.ts
20212
20594
  function normalizeLineEndings(text) {
@@ -21075,7 +21457,7 @@ function isMissingPathError(error) {
21075
21457
  }
21076
21458
  async function real(target) {
21077
21459
  const parts = [];
21078
- let current = path3.resolve(target);
21460
+ let current = path4.resolve(target);
21079
21461
  while (true) {
21080
21462
  const exact = await fs3.realpath(current).catch((error) => {
21081
21463
  if (isMissingPathError(error)) {
@@ -21084,19 +21466,19 @@ async function real(target) {
21084
21466
  throw createApplyPatchInternalError(`Failed to resolve real path: ${current}`, error);
21085
21467
  });
21086
21468
  if (exact) {
21087
- return parts.length === 0 ? exact : path3.join(exact, ...parts.reverse());
21469
+ return parts.length === 0 ? exact : path4.join(exact, ...parts.reverse());
21088
21470
  }
21089
- const parent = path3.dirname(current);
21471
+ const parent = path4.dirname(current);
21090
21472
  if (parent === current) {
21091
- return parts.length === 0 ? current : path3.join(current, ...parts.reverse());
21473
+ return parts.length === 0 ? current : path4.join(current, ...parts.reverse());
21092
21474
  }
21093
- parts.push(path3.basename(current));
21475
+ parts.push(path4.basename(current));
21094
21476
  current = parent;
21095
21477
  }
21096
21478
  }
21097
21479
  function inside(root, target) {
21098
- const rel = path3.relative(root, target);
21099
- return rel === "" || !rel.startsWith("..") && !path3.isAbsolute(rel);
21480
+ const rel = path4.relative(root, target);
21481
+ return rel === "" || !rel.startsWith("..") && !path4.isAbsolute(rel);
21100
21482
  }
21101
21483
  function createPathGuardContext(root, worktree) {
21102
21484
  return {
@@ -21106,7 +21488,7 @@ function createPathGuardContext(root, worktree) {
21106
21488
  };
21107
21489
  }
21108
21490
  async function realCached(ctx, target) {
21109
- const resolvedTarget = path3.resolve(target);
21491
+ const resolvedTarget = path4.resolve(target);
21110
21492
  let pending = ctx.realCache.get(resolvedTarget);
21111
21493
  if (!pending) {
21112
21494
  pending = real(resolvedTarget);
@@ -21157,22 +21539,22 @@ async function assertRegularFile(ctx, filePath, verb) {
21157
21539
  function collectPatchTargets(root, hunks) {
21158
21540
  const targets = new Set;
21159
21541
  for (const hunk of hunks) {
21160
- targets.add(path3.resolve(root, hunk.path));
21542
+ targets.add(path4.resolve(root, hunk.path));
21161
21543
  if (hunk.type === "update" && hunk.move_path) {
21162
- targets.add(path3.resolve(root, hunk.move_path));
21544
+ targets.add(path4.resolve(root, hunk.move_path));
21163
21545
  }
21164
21546
  }
21165
21547
  return [...targets];
21166
21548
  }
21167
21549
  function toRelativePatchPath(root, target) {
21168
- const relative = path3.relative(root, target);
21550
+ const relative = path4.relative(root, target);
21169
21551
  return (relative.length === 0 ? "." : relative).replaceAll("\\", "/");
21170
21552
  }
21171
21553
  function normalizePatchPath(root, value) {
21172
- return path3.isAbsolute(value) ? toRelativePatchPath(root, path3.resolve(value)) : value;
21554
+ return path4.isAbsolute(value) ? toRelativePatchPath(root, path4.resolve(value)) : value;
21173
21555
  }
21174
21556
  function normalizePatchPaths(root, hunks) {
21175
- const resolvedRoot = path3.resolve(root);
21557
+ const resolvedRoot = path4.resolve(root);
21176
21558
  const normalized = [];
21177
21559
  let changed = false;
21178
21560
  for (const hunk of hunks) {
@@ -21296,7 +21678,7 @@ function stageAddedText(contents) {
21296
21678
  `;
21297
21679
  }
21298
21680
  // src/hooks/apply-patch/rewrite.ts
21299
- import path4 from "node:path";
21681
+ import path5 from "node:path";
21300
21682
  function normalizeTextLineEndings(text) {
21301
21683
  return text.replace(/\r\n/g, `
21302
21684
  `).replace(/\r/g, `
@@ -21453,7 +21835,7 @@ async function rewritePatch(root, patchText, cfg, worktree) {
21453
21835
  const dependencyGroups = new Map;
21454
21836
  for (const hunk of hunks) {
21455
21837
  if (hunk.type === "add") {
21456
- const filePath2 = path4.resolve(root, hunk.path);
21838
+ const filePath2 = path5.resolve(root, hunk.path);
21457
21839
  await assertPreparedPathMissing(filePath2, "add");
21458
21840
  rewritten.push(hunk);
21459
21841
  clearDependencyGroup(filePath2);
@@ -21475,20 +21857,20 @@ async function rewritePatch(root, patchText, cfg, worktree) {
21475
21857
  continue;
21476
21858
  }
21477
21859
  if (hunk.type === "delete") {
21478
- const filePath2 = path4.resolve(root, hunk.path);
21860
+ const filePath2 = path5.resolve(root, hunk.path);
21479
21861
  await getPreparedFileState(filePath2, "delete");
21480
21862
  clearDependencyGroup(filePath2);
21481
21863
  rewritten.push(hunk);
21482
21864
  staged.set(filePath2, { exists: false, derived: true });
21483
21865
  continue;
21484
21866
  }
21485
- const filePath = path4.resolve(root, hunk.path);
21867
+ const filePath = path5.resolve(root, hunk.path);
21486
21868
  const currentDependency = dependencyGroups.get(filePath);
21487
21869
  const current = await getPreparedFileState(filePath, "update");
21488
21870
  if (!current.exists) {
21489
21871
  throw createApplyPatchVerificationError(`Failed to read file to update: ${filePath}`);
21490
21872
  }
21491
- 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;
21492
21874
  if (movePath && movePath !== filePath) {
21493
21875
  await assertPreparedPathMissing(movePath, "move");
21494
21876
  }
@@ -21682,32 +22064,32 @@ function crossSpawn(command, options) {
21682
22064
  }
21683
22065
  };
21684
22066
  }
21685
- async function crossWrite(path5, data) {
21686
- await fsWriteFile(path5, Buffer.from(data));
22067
+ async function crossWrite(path6, data) {
22068
+ await fsWriteFile(path6, Buffer.from(data));
21687
22069
  }
21688
22070
 
21689
22071
  // src/hooks/auto-update-checker/cache.ts
21690
22072
  import * as fs5 from "node:fs";
21691
- import * as path7 from "node:path";
22073
+ import * as path8 from "node:path";
21692
22074
  // src/hooks/auto-update-checker/checker.ts
21693
22075
  import * as fs4 from "node:fs";
21694
- import * as path6 from "node:path";
21695
- import { fileURLToPath } from "node:url";
22076
+ import * as path7 from "node:path";
22077
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
21696
22078
 
21697
22079
  // src/hooks/auto-update-checker/constants.ts
21698
- import * as os2 from "node:os";
21699
- import * as path5 from "node:path";
22080
+ import * as os3 from "node:os";
22081
+ import * as path6 from "node:path";
21700
22082
  var PACKAGE_NAME = "oh-my-opencode-slim";
21701
22083
  var NPM_REGISTRY_URL = `https://registry.npmjs.org/-/package/${PACKAGE_NAME}/dist-tags`;
21702
22084
  var NPM_FETCH_TIMEOUT = 5000;
21703
22085
  function getCacheDir() {
21704
22086
  if (process.platform === "win32") {
21705
- return path5.join(process.env.LOCALAPPDATA ?? os2.homedir(), "opencode");
22087
+ return path6.join(process.env.LOCALAPPDATA ?? os3.homedir(), "opencode");
21706
22088
  }
21707
- return path5.join(os2.homedir(), ".cache", "opencode");
22089
+ return path6.join(os3.homedir(), ".cache", "opencode");
21708
22090
  }
21709
22091
  var CACHE_DIR = getCacheDir();
21710
- 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");
21711
22093
  var configPaths = getOpenCodeConfigPaths();
21712
22094
  var USER_OPENCODE_CONFIG = configPaths[0];
21713
22095
  var USER_OPENCODE_CONFIG_JSONC = configPaths[1];
@@ -21742,8 +22124,8 @@ function extractChannel(version) {
21742
22124
  }
21743
22125
  function getConfigPaths(directory) {
21744
22126
  return [
21745
- path6.join(directory, ".opencode", "opencode.json"),
21746
- path6.join(directory, ".opencode", "opencode.jsonc"),
22127
+ path7.join(directory, ".opencode", "opencode.json"),
22128
+ path7.join(directory, ".opencode", "opencode.jsonc"),
21747
22129
  USER_OPENCODE_CONFIG,
21748
22130
  USER_OPENCODE_CONFIG_JSONC
21749
22131
  ];
@@ -21759,7 +22141,7 @@ function getLocalDevPath(directory) {
21759
22141
  for (const entry of plugins) {
21760
22142
  if (entry.startsWith("file://") && entry.includes(PACKAGE_NAME)) {
21761
22143
  try {
21762
- return fileURLToPath(entry);
22144
+ return fileURLToPath2(entry);
21763
22145
  } catch {
21764
22146
  return entry.replace("file://", "");
21765
22147
  }
@@ -21772,9 +22154,9 @@ function getLocalDevPath(directory) {
21772
22154
  function findPackageJsonUp(startPath) {
21773
22155
  try {
21774
22156
  const stat2 = fs4.statSync(startPath);
21775
- let dir = stat2.isDirectory() ? startPath : path6.dirname(startPath);
22157
+ let dir = stat2.isDirectory() ? startPath : path7.dirname(startPath);
21776
22158
  for (let i = 0;i < 10; i++) {
21777
- const pkgPath = path6.join(dir, "package.json");
22159
+ const pkgPath = path7.join(dir, "package.json");
21778
22160
  if (fs4.existsSync(pkgPath)) {
21779
22161
  try {
21780
22162
  const content = fs4.readFileSync(pkgPath, "utf-8");
@@ -21783,7 +22165,7 @@ function findPackageJsonUp(startPath) {
21783
22165
  return pkgPath;
21784
22166
  } catch {}
21785
22167
  }
21786
- const parent = path6.dirname(dir);
22168
+ const parent = path7.dirname(dir);
21787
22169
  if (parent === dir)
21788
22170
  break;
21789
22171
  dir = parent;
@@ -21808,7 +22190,7 @@ function getLocalDevVersion(directory) {
21808
22190
  }
21809
22191
  function getCurrentRuntimePackageJsonPath(currentModuleUrl = import.meta.url) {
21810
22192
  try {
21811
- const currentDir = path6.dirname(fileURLToPath(currentModuleUrl));
22193
+ const currentDir = path7.dirname(fileURLToPath2(currentModuleUrl));
21812
22194
  return findPackageJsonUp(currentDir);
21813
22195
  } catch (err) {
21814
22196
  log("[auto-update-checker] Failed to resolve runtime package path:", err);
@@ -21892,7 +22274,7 @@ async function getLatestVersion(channel = "latest") {
21892
22274
 
21893
22275
  // src/hooks/auto-update-checker/cache.ts
21894
22276
  function removeFromBunLock(installDir, packageName) {
21895
- const lockPath = path7.join(installDir, "bun.lock");
22277
+ const lockPath = path8.join(installDir, "bun.lock");
21896
22278
  if (!fs5.existsSync(lockPath))
21897
22279
  return false;
21898
22280
  try {
@@ -21943,7 +22325,7 @@ function ensureDependencyVersion(packageJsonPath, packageName, version) {
21943
22325
  }
21944
22326
  }
21945
22327
  function removeInstalledPackage(installDir, packageName) {
21946
- const pkgDir = path7.join(installDir, "node_modules", packageName);
22328
+ const pkgDir = path8.join(installDir, "node_modules", packageName);
21947
22329
  if (!fs5.existsSync(pkgDir))
21948
22330
  return false;
21949
22331
  fs5.rmSync(pkgDir, { recursive: true, force: true });
@@ -21952,18 +22334,18 @@ function removeInstalledPackage(installDir, packageName) {
21952
22334
  }
21953
22335
  function resolveInstallContext(runtimePackageJsonPath = getCurrentRuntimePackageJsonPath()) {
21954
22336
  if (runtimePackageJsonPath) {
21955
- const packageDir = path7.dirname(runtimePackageJsonPath);
21956
- const nodeModulesDir = path7.dirname(packageDir);
21957
- if (path7.basename(packageDir) === PACKAGE_NAME && path7.basename(nodeModulesDir) === "node_modules") {
21958
- const installDir = path7.dirname(nodeModulesDir);
21959
- 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");
21960
22342
  if (fs5.existsSync(packageJsonPath)) {
21961
22343
  return { installDir, packageJsonPath };
21962
22344
  }
21963
22345
  }
21964
22346
  return null;
21965
22347
  }
21966
- const legacyPackageJsonPath = path7.join(CACHE_DIR, "package.json");
22348
+ const legacyPackageJsonPath = path8.join(CACHE_DIR, "package.json");
21967
22349
  if (fs5.existsSync(legacyPackageJsonPath)) {
21968
22350
  return { installDir: CACHE_DIR, packageJsonPath: legacyPackageJsonPath };
21969
22351
  }
@@ -22097,7 +22479,7 @@ function showToast(ctx, title, message, variant = "info", duration = 3000) {
22097
22479
  }).catch(() => {});
22098
22480
  }
22099
22481
  // src/utils/agent-variant.ts
22100
- function normalizeAgentName(agentName) {
22482
+ function normalizeAgentName2(agentName) {
22101
22483
  const trimmed = agentName.trim();
22102
22484
  return trimmed.startsWith("@") ? trimmed.slice(1) : trimmed;
22103
22485
  }
@@ -22109,7 +22491,7 @@ function getRuntimeAgentNames(config) {
22109
22491
  return [...unique];
22110
22492
  }
22111
22493
  function resolveRuntimeAgentName(config, agentName) {
22112
- const normalized = normalizeAgentName(agentName);
22494
+ const normalized = normalizeAgentName2(agentName);
22113
22495
  if (!normalized) {
22114
22496
  return normalized;
22115
22497
  }
@@ -22121,7 +22503,7 @@ function resolveRuntimeAgentName(config, agentName) {
22121
22503
  if (!displayName) {
22122
22504
  continue;
22123
22505
  }
22124
- if (normalizeAgentName(displayName) === normalized) {
22506
+ if (normalizeAgentName2(displayName) === normalized) {
22125
22507
  return internalName;
22126
22508
  }
22127
22509
  }
@@ -22137,7 +22519,7 @@ function createDisplayNameMentionRewriter(config) {
22137
22519
  if (!displayName) {
22138
22520
  continue;
22139
22521
  }
22140
- const normalizedDisplayName = normalizeAgentName(displayName);
22522
+ const normalizedDisplayName = normalizeAgentName2(displayName);
22141
22523
  if (!normalizedDisplayName || normalizedDisplayName === internalName) {
22142
22524
  continue;
22143
22525
  }
@@ -22447,8 +22829,8 @@ function isPwshAvailable() {
22447
22829
  });
22448
22830
  return result.status === 0;
22449
22831
  }
22450
- function escapePowerShellPath(path8) {
22451
- return path8.replace(/'/g, "''");
22832
+ function escapePowerShellPath(path9) {
22833
+ return path9.replace(/'/g, "''");
22452
22834
  }
22453
22835
  function getWindowsZipExtractor() {
22454
22836
  const buildNumber = getWindowsBuildNumber();
@@ -22777,6 +23159,7 @@ function parseModel(model) {
22777
23159
  return { providerID: model.slice(0, slash), modelID: model.slice(slash + 1) };
22778
23160
  }
22779
23161
  var DEDUP_WINDOW_MS = 5000;
23162
+ var REPROMPT_DELAY_MS = 500;
22780
23163
 
22781
23164
  class ForegroundFallbackManager {
22782
23165
  client;
@@ -22909,11 +23292,20 @@ class ForegroundFallbackManager {
22909
23292
  log("[foreground-fallback] no user message found", { sessionID });
22910
23293
  return;
22911
23294
  }
22912
- try {
22913
- await this.client.session.abort({ path: { id: sessionID } });
22914
- } catch {}
22915
- await new Promise((r) => setTimeout(r, 500));
22916
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));
22917
23309
  await sessionClient.promptAsync({
22918
23310
  path: { id: sessionID },
22919
23311
  body: { parts: lastUser.parts, model: ref }
@@ -22960,8 +23352,8 @@ class ForegroundFallbackManager {
22960
23352
  // src/hooks/image-hook.ts
22961
23353
  import { createHash } from "node:crypto";
22962
23354
  import {
22963
- existsSync as existsSync4,
22964
- mkdirSync as mkdirSync2,
23355
+ existsSync as existsSync5,
23356
+ mkdirSync as mkdirSync3,
22965
23357
  readdirSync as readdirSync2,
22966
23358
  rmdirSync,
22967
23359
  statSync as statSync3,
@@ -23056,7 +23448,7 @@ function writeUniqueFile(dir, name, data, log2) {
23056
23448
  const ext = extname(name);
23057
23449
  const base = basename2(name, ext) || name;
23058
23450
  let candidate = join7(dir, name);
23059
- if (existsSync4(candidate)) {
23451
+ if (existsSync5(candidate)) {
23060
23452
  return candidate;
23061
23453
  }
23062
23454
  let counter = 0;
@@ -23094,14 +23486,14 @@ function processImageAttachments(args) {
23094
23486
  }
23095
23487
  const saveDir = join7(workDir, ".opencode", "images");
23096
23488
  if (messagesWithImages.length === 0) {
23097
- if (existsSync4(saveDir))
23489
+ if (existsSync5(saveDir))
23098
23490
  cleanupAllSessions(saveDir);
23099
23491
  return;
23100
23492
  }
23101
23493
  const gitignorePath = join7(workDir, ".opencode", ".gitignore");
23102
23494
  try {
23103
- mkdirSync2(saveDir, { recursive: true });
23104
- if (!existsSync4(gitignorePath))
23495
+ mkdirSync3(saveDir, { recursive: true });
23496
+ if (!existsSync5(gitignorePath))
23105
23497
  writeFileSync3(gitignorePath, `*
23106
23498
  `);
23107
23499
  } catch (e) {
@@ -23112,7 +23504,7 @@ function processImageAttachments(args) {
23112
23504
  const sessionSubdir = msg.info.sessionID ? sanitizeFilename(msg.info.sessionID) : undefined;
23113
23505
  const targetDir = sessionSubdir ? join7(saveDir, sessionSubdir) : saveDir;
23114
23506
  try {
23115
- mkdirSync2(targetDir, { recursive: true });
23507
+ mkdirSync3(targetDir, { recursive: true });
23116
23508
  } catch (e) {
23117
23509
  log2(`[image-hook] failed to create target image directory: ${e}`);
23118
23510
  }
@@ -23271,7 +23663,7 @@ function createPostFileToolNudgeHook(options = {}) {
23271
23663
  };
23272
23664
  }
23273
23665
  // src/hooks/task-session-manager/index.ts
23274
- import path8 from "node:path";
23666
+ import path9 from "node:path";
23275
23667
  var AGENT_NAME_SET = new Set([
23276
23668
  "orchestrator",
23277
23669
  "oracle",
@@ -23296,8 +23688,8 @@ function extractPath(output) {
23296
23688
  return /<path>([^<]+)<\/path>/.exec(output)?.[1];
23297
23689
  }
23298
23690
  function normalizePath(root, file) {
23299
- const relative = path8.relative(root, file);
23300
- if (!relative || relative.startsWith("..") || path8.isAbsolute(relative)) {
23691
+ const relative = path9.relative(root, file);
23692
+ if (!relative || relative.startsWith("..") || path9.isAbsolute(relative)) {
23301
23693
  return file;
23302
23694
  }
23303
23695
  return relative;
@@ -23689,6 +24081,7 @@ function createTodoHygiene(options) {
23689
24081
  // src/hooks/todo-continuation/index.ts
23690
24082
  var HOOK_NAME = "todo-continuation";
23691
24083
  var COMMAND_NAME = "auto-continue";
24084
+ var TODO_STATE_TIMEOUT_MS = 500;
23692
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.]";
23693
24086
  var TODO_HYGIENE_INSTRUCTION_OPEN = '<instruction name="todo_hygiene">';
23694
24087
  var TODO_HYGIENE_INSTRUCTION_CLOSE = "</instruction>";
@@ -23776,12 +24169,15 @@ function createTodoContinuationHook(ctx, config) {
23776
24169
  notifyingSessionIds: new Set,
23777
24170
  notificationBusyUntilBySession: new Map
23778
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
+ }
23779
24178
  const hygiene = createTodoHygiene({
23780
24179
  getTodoState: async (sessionID) => {
23781
- const result = await ctx.client.session.todo({
23782
- path: { id: sessionID }
23783
- });
23784
- const todos = result.data;
24180
+ const todos = await fetchTodos(sessionID);
23785
24181
  const openTodos = todos.filter((todo) => !TERMINAL_TODO_STATUSES.includes(todo.status));
23786
24182
  return {
23787
24183
  hasOpenTodos: openTodos.length > 0,
@@ -23966,10 +24362,7 @@ function createTodoContinuationHook(ctx, config) {
23966
24362
  }
23967
24363
  if (autoEnable && !state.enabled) {
23968
24364
  try {
23969
- const todosResult = await ctx.client.session.todo({
23970
- path: { id: sessionID }
23971
- });
23972
- const todos = todosResult.data;
24365
+ const todos = await fetchTodos(sessionID);
23973
24366
  const incompleteCount2 = todos.filter((t) => !TERMINAL_TODO_STATUSES.includes(t.status)).length;
23974
24367
  if (incompleteCount2 >= autoEnableThreshold) {
23975
24368
  state.enabled = true;
@@ -23995,10 +24388,7 @@ function createTodoContinuationHook(ctx, config) {
23995
24388
  let hasIncompleteTodos = false;
23996
24389
  let incompleteCount = 0;
23997
24390
  try {
23998
- const todosResult = await ctx.client.session.todo({
23999
- path: { id: sessionID }
24000
- });
24001
- const todos = todosResult.data;
24391
+ const todos = await fetchTodos(sessionID);
24002
24392
  incompleteCount = todos.filter((t) => !TERMINAL_TODO_STATUSES.includes(t.status)).length;
24003
24393
  hasIncompleteTodos = incompleteCount > 0;
24004
24394
  log(`[${HOOK_NAME}] Fetched todos`, {
@@ -24209,10 +24599,7 @@ function createTodoContinuationHook(ctx, config) {
24209
24599
  });
24210
24600
  let hasIncompleteTodos = false;
24211
24601
  try {
24212
- const todosResult = await ctx.client.session.todo({
24213
- path: { id: input.sessionID }
24214
- });
24215
- const todos = todosResult.data;
24602
+ const todos = await fetchTodos(input.sessionID);
24216
24603
  hasIncompleteTodos = todos.some((t) => !TERMINAL_TODO_STATUSES.includes(t.status));
24217
24604
  } catch (error) {
24218
24605
  log(`[${HOOK_NAME}] Warning: failed to fetch todos in command hook`, {
@@ -24236,7 +24623,7 @@ function createTodoContinuationHook(ctx, config) {
24236
24623
  };
24237
24624
  }
24238
24625
  // src/interview/manager.ts
24239
- import path12 from "node:path";
24626
+ import path13 from "node:path";
24240
24627
 
24241
24628
  // src/interview/dashboard.ts
24242
24629
  import crypto from "node:crypto";
@@ -24245,28 +24632,28 @@ import fs7 from "node:fs/promises";
24245
24632
  import {
24246
24633
  createServer
24247
24634
  } from "node:http";
24248
- import os3 from "node:os";
24249
- import path10 from "node:path";
24635
+ import os4 from "node:os";
24636
+ import path11 from "node:path";
24250
24637
  import { URL as URL2 } from "node:url";
24251
24638
 
24252
24639
  // src/interview/document.ts
24253
24640
  import * as fsSync from "node:fs";
24254
24641
  import * as fs6 from "node:fs/promises";
24255
- import * as path9 from "node:path";
24642
+ import * as path10 from "node:path";
24256
24643
  var DEFAULT_OUTPUT_FOLDER = "interview";
24257
24644
  function normalizeOutputFolder(outputFolder) {
24258
24645
  const normalized = outputFolder.trim().replace(/^\/+|\/+$/g, "");
24259
24646
  return normalized || DEFAULT_OUTPUT_FOLDER;
24260
24647
  }
24261
24648
  function createInterviewDirectoryPath(directory, outputFolder) {
24262
- return path9.join(directory, normalizeOutputFolder(outputFolder));
24649
+ return path10.join(directory, normalizeOutputFolder(outputFolder));
24263
24650
  }
24264
24651
  function createInterviewFilePath(directory, outputFolder, idea) {
24265
24652
  const fileName = `${slugify(idea) || "interview"}.md`;
24266
- return path9.join(createInterviewDirectoryPath(directory, outputFolder), fileName);
24653
+ return path10.join(createInterviewDirectoryPath(directory, outputFolder), fileName);
24267
24654
  }
24268
24655
  function relativeInterviewPath(directory, filePath) {
24269
- return path9.relative(directory, filePath) || path9.basename(filePath);
24656
+ return path10.relative(directory, filePath) || path10.basename(filePath);
24270
24657
  }
24271
24658
  function resolveExistingInterviewPath(directory, outputFolder, value) {
24272
24659
  const trimmed = value.trim();
@@ -24275,22 +24662,22 @@ function resolveExistingInterviewPath(directory, outputFolder, value) {
24275
24662
  }
24276
24663
  const outputDir = createInterviewDirectoryPath(directory, outputFolder);
24277
24664
  const candidates = new Set;
24278
- const resolvedRoot = path9.resolve(directory);
24279
- if (path9.isAbsolute(trimmed)) {
24665
+ const resolvedRoot = path10.resolve(directory);
24666
+ if (path10.isAbsolute(trimmed)) {
24280
24667
  candidates.add(trimmed);
24281
24668
  } else {
24282
- candidates.add(path9.resolve(directory, trimmed));
24283
- candidates.add(path9.join(outputDir, trimmed));
24669
+ candidates.add(path10.resolve(directory, trimmed));
24670
+ candidates.add(path10.join(outputDir, trimmed));
24284
24671
  if (!trimmed.endsWith(".md")) {
24285
- candidates.add(path9.join(outputDir, `${trimmed}.md`));
24672
+ candidates.add(path10.join(outputDir, `${trimmed}.md`));
24286
24673
  }
24287
24674
  }
24288
24675
  for (const candidate of candidates) {
24289
- if (path9.extname(candidate) !== ".md") {
24676
+ if (path10.extname(candidate) !== ".md") {
24290
24677
  continue;
24291
24678
  }
24292
- const resolved = path9.resolve(candidate);
24293
- if (!resolved.startsWith(resolvedRoot + path9.sep) && resolved !== resolvedRoot) {
24679
+ const resolved = path10.resolve(candidate);
24680
+ if (!resolved.startsWith(resolvedRoot + path10.sep) && resolved !== resolvedRoot) {
24294
24681
  continue;
24295
24682
  }
24296
24683
  if (fsSync.existsSync(candidate)) {
@@ -24370,7 +24757,7 @@ function parseFrontmatter(content) {
24370
24757
  return result;
24371
24758
  }
24372
24759
  async function ensureInterviewFile(record) {
24373
- await fs6.mkdir(path9.dirname(record.markdownPath), { recursive: true });
24760
+ await fs6.mkdir(path10.dirname(record.markdownPath), { recursive: true });
24374
24761
  try {
24375
24762
  await fs6.access(record.markdownPath);
24376
24763
  } catch {
@@ -26040,12 +26427,12 @@ function renderInterviewPage(interviewId, resumeSlug) {
26040
26427
 
26041
26428
  // src/interview/dashboard.ts
26042
26429
  function getAuthFilePath(port) {
26043
- const dataHome = process.env.XDG_DATA_HOME || path10.join(os3.homedir(), ".local", "share");
26044
- 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`);
26045
26432
  }
26046
26433
  function writeAuthFile(port, token) {
26047
26434
  const filePath = getAuthFilePath(port);
26048
- const dir = path10.dirname(filePath);
26435
+ const dir = path11.dirname(filePath);
26049
26436
  try {
26050
26437
  fsSync2.mkdirSync(dir, { recursive: true });
26051
26438
  } catch {}
@@ -26142,7 +26529,7 @@ function createDashboardServer(config) {
26142
26529
  let scanDays = 30;
26143
26530
  function getKnownDirectories() {
26144
26531
  const dirs = new Set;
26145
- dirs.add(os3.homedir());
26532
+ dirs.add(os4.homedir());
26146
26533
  for (const session2 of sessions.values()) {
26147
26534
  if (session2.directory)
26148
26535
  dirs.add(session2.directory);
@@ -26182,7 +26569,7 @@ function createDashboardServer(config) {
26182
26569
  const directories = getKnownDirectories();
26183
26570
  const items = [];
26184
26571
  for (const dir of directories) {
26185
- const interviewDir = path10.join(dir, config.outputFolder);
26572
+ const interviewDir = path11.join(dir, config.outputFolder);
26186
26573
  let entries;
26187
26574
  try {
26188
26575
  entries = await fs7.readdir(interviewDir);
@@ -26194,7 +26581,7 @@ function createDashboardServer(config) {
26194
26581
  continue;
26195
26582
  let content;
26196
26583
  try {
26197
- content = await fs7.readFile(path10.join(interviewDir, entry), "utf8");
26584
+ content = await fs7.readFile(path11.join(interviewDir, entry), "utf8");
26198
26585
  } catch {
26199
26586
  continue;
26200
26587
  }
@@ -26220,7 +26607,7 @@ function createDashboardServer(config) {
26220
26607
  const directories = getKnownDirectories();
26221
26608
  let rebuilt = 0;
26222
26609
  for (const dir of directories) {
26223
- const interviewDir = path10.join(dir, config.outputFolder);
26610
+ const interviewDir = path11.join(dir, config.outputFolder);
26224
26611
  let entries;
26225
26612
  try {
26226
26613
  entries = await fs7.readdir(interviewDir);
@@ -26232,7 +26619,7 @@ function createDashboardServer(config) {
26232
26619
  continue;
26233
26620
  let content;
26234
26621
  try {
26235
- content = await fs7.readFile(path10.join(interviewDir, entry), "utf8");
26622
+ content = await fs7.readFile(path11.join(interviewDir, entry), "utf8");
26236
26623
  } catch {
26237
26624
  continue;
26238
26625
  }
@@ -26258,7 +26645,7 @@ function createDashboardServer(config) {
26258
26645
  questions: [],
26259
26646
  pendingAnswers: null,
26260
26647
  lastUpdatedAt: fm.updatedAt ? new Date(fm.updatedAt).getTime() : Date.now(),
26261
- filePath: path10.join(interviewDir, entry),
26648
+ filePath: path11.join(interviewDir, entry),
26262
26649
  nudgeAction: null
26263
26650
  });
26264
26651
  if (!sessions.has(fm.sessionID)) {
@@ -26522,7 +26909,7 @@ function createDashboardServer(config) {
26522
26909
  const dirs = getKnownDirectories();
26523
26910
  for (const dir of dirs) {
26524
26911
  const slug = extractResumeSlug(interviewId);
26525
- const candidate = path10.join(dir, config.outputFolder, `${slug}.md`);
26912
+ const candidate = path11.join(dir, config.outputFolder, `${slug}.md`);
26526
26913
  try {
26527
26914
  document = await fs7.readFile(candidate, "utf8");
26528
26915
  markdownPath = candidate;
@@ -27048,9 +27435,9 @@ function createInterviewServer(deps) {
27048
27435
  }
27049
27436
 
27050
27437
  // src/interview/service.ts
27051
- import { spawn } from "node:child_process";
27438
+ import { spawn as spawn2 } from "node:child_process";
27052
27439
  import * as fs8 from "node:fs/promises";
27053
- import * as path11 from "node:path";
27440
+ import * as path12 from "node:path";
27054
27441
 
27055
27442
  // src/interview/types.ts
27056
27443
  import { z as z3 } from "zod";
@@ -27252,7 +27639,7 @@ function openBrowser(url) {
27252
27639
  args = [url];
27253
27640
  }
27254
27641
  try {
27255
- const child = spawn(command, args, { detached: true, stdio: "ignore" });
27642
+ const child = spawn2(command, args, { detached: true, stdio: "ignore" });
27256
27643
  child.on("error", (error) => {
27257
27644
  log("[interview] failed to open browser:", { error: error.message, url });
27258
27645
  });
@@ -27317,12 +27704,12 @@ function createInterviewService(ctx, config, deps) {
27317
27704
  if (!newSlug) {
27318
27705
  return;
27319
27706
  }
27320
- const currentFileName = path11.basename(interview.markdownPath, ".md");
27707
+ const currentFileName = path12.basename(interview.markdownPath, ".md");
27321
27708
  if (currentFileName === newSlug) {
27322
27709
  return;
27323
27710
  }
27324
- const dir = path11.dirname(interview.markdownPath);
27325
- const newPath = path11.join(dir, `${newSlug}.md`);
27711
+ const dir = path12.dirname(interview.markdownPath);
27712
+ const newPath = path12.join(dir, `${newSlug}.md`);
27326
27713
  try {
27327
27714
  await fs8.access(newPath);
27328
27715
  return;
@@ -27398,9 +27785,9 @@ function createInterviewService(ctx, config, deps) {
27398
27785
  const messages = await loadMessages(sessionID);
27399
27786
  const title = extractTitle(document);
27400
27787
  const record = {
27401
- id: `${Date.now()}-${++idCounter}-${slugify(path11.basename(markdownPath, ".md")) || "interview"}`,
27788
+ id: `${Date.now()}-${++idCounter}-${slugify(path12.basename(markdownPath, ".md")) || "interview"}`,
27402
27789
  sessionID,
27403
- idea: title || path11.basename(markdownPath, ".md"),
27790
+ idea: title || path12.basename(markdownPath, ".md"),
27404
27791
  markdownPath,
27405
27792
  createdAt: nowIso(),
27406
27793
  status: "active",
@@ -27627,7 +28014,7 @@ function createInterviewService(ctx, config, deps) {
27627
28014
  return fileCache.items;
27628
28015
  }
27629
28016
  const outputDir = createInterviewDirectoryPath(ctx.directory, outputFolder);
27630
- 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)));
27631
28018
  let entries;
27632
28019
  try {
27633
28020
  entries = await fs8.readdir(outputDir);
@@ -27638,8 +28025,8 @@ function createInterviewService(ctx, config, deps) {
27638
28025
  for (const entry of entries) {
27639
28026
  if (!entry.endsWith(".md"))
27640
28027
  continue;
27641
- const fullPath = path11.join(outputDir, entry);
27642
- if (activePaths.has(path11.resolve(fullPath)))
28028
+ const fullPath = path12.join(outputDir, entry);
28029
+ if (activePaths.has(path12.resolve(fullPath)))
27643
28030
  continue;
27644
28031
  let content;
27645
28032
  try {
@@ -27738,7 +28125,7 @@ function createInterviewManager(ctx, config) {
27738
28125
  const outputFolder = interviewConfig?.outputFolder ?? "interview";
27739
28126
  if (!dashboardEnabled) {
27740
28127
  const service2 = createInterviewService(ctx, interviewConfig);
27741
- const resolvedOutputPath = path12.join(ctx.directory, outputFolder);
28128
+ const resolvedOutputPath = path13.join(ctx.directory, outputFolder);
27742
28129
  const server = createInterviewServer({
27743
28130
  getState: async (interviewId) => service2.getInterviewState(interviewId),
27744
28131
  listInterviewFiles: async () => service2.listInterviewFiles(),
@@ -27843,7 +28230,7 @@ function createInterviewManager(ctx, config) {
27843
28230
  listInterviews: () => service.listInterviews(),
27844
28231
  submitAnswers: async (interviewId, answers) => service.submitAnswers(interviewId, answers),
27845
28232
  handleNudgeAction: async (interviewId, action) => service.handleNudgeAction(interviewId, action),
27846
- outputFolder: path12.join(ctx.directory, outputFolder),
28233
+ outputFolder: path13.join(ctx.directory, outputFolder),
27847
28234
  port: 0
27848
28235
  });
27849
28236
  service.setBaseUrlResolver(() => perSessionServer.ensureStarted());
@@ -28111,6 +28498,8 @@ function createBuiltinMcps(disabledMcps = [], websearchConfig) {
28111
28498
  }
28112
28499
 
28113
28500
  // src/multiplexer/tmux/index.ts
28501
+ var TMUX_LAYOUT_DEBOUNCE_MS = 150;
28502
+
28114
28503
  class TmuxMultiplexer {
28115
28504
  type = "tmux";
28116
28505
  binaryPath = null;
@@ -28118,6 +28507,8 @@ class TmuxMultiplexer {
28118
28507
  storedLayout;
28119
28508
  storedMainPaneSize;
28120
28509
  targetPane = process.env.TMUX_PANE;
28510
+ layoutTimer;
28511
+ layoutGeneration = 0;
28121
28512
  constructor(layout = "main-vertical", mainPaneSize = 60) {
28122
28513
  this.storedLayout = layout;
28123
28514
  this.storedMainPaneSize = mainPaneSize;
@@ -28179,7 +28570,7 @@ class TmuxMultiplexer {
28179
28570
  if (exitCode === 0 && paneId) {
28180
28571
  const renameProc = crossSpawn([tmux, "select-pane", "-t", paneId, "-T", description.slice(0, 30)], { stdout: "ignore", stderr: "ignore" });
28181
28572
  await renameProc.exited;
28182
- await this.applyLayout(this.storedLayout, this.storedMainPaneSize);
28573
+ this.scheduleLayout();
28183
28574
  log("[tmux] spawnPane: SUCCESS", { paneId });
28184
28575
  return { success: true, paneId };
28185
28576
  }
@@ -28216,7 +28607,7 @@ class TmuxMultiplexer {
28216
28607
  const stderr = await proc.stderr();
28217
28608
  log("[tmux] closePane: result", { exitCode, stderr: stderr.trim() });
28218
28609
  if (exitCode === 0) {
28219
- await this.applyLayout(this.storedLayout, this.storedMainPaneSize);
28610
+ this.scheduleLayout();
28220
28611
  return true;
28221
28612
  }
28222
28613
  log("[tmux] closePane: failed (pane may already be closed)", { paneId });
@@ -28227,41 +28618,74 @@ class TmuxMultiplexer {
28227
28618
  }
28228
28619
  }
28229
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) {
28230
28641
  const tmux = await this.getBinary();
28231
28642
  if (!tmux)
28232
28643
  return;
28233
28644
  this.storedLayout = layout;
28234
28645
  this.storedMainPaneSize = mainPaneSize;
28235
28646
  try {
28236
- const layoutProc = crossSpawn([tmux, "select-layout", ...this.targetArgs(), layout], {
28237
- stdout: "pipe",
28238
- stderr: "pipe"
28239
- });
28240
- await layoutProc.exited;
28647
+ const layoutResult = await this.runTmux(tmux, ["select-layout", ...this.targetArgs(), layout]);
28648
+ if (layoutResult !== 0)
28649
+ return;
28241
28650
  if (layout === "main-horizontal" || layout === "main-vertical") {
28242
28651
  const sizeOption = layout === "main-horizontal" ? "main-pane-height" : "main-pane-width";
28243
- const sizeProc = crossSpawn([
28244
- tmux,
28652
+ const sizeResult = await this.runTmux(tmux, [
28245
28653
  "set-window-option",
28246
28654
  ...this.targetArgs(),
28247
28655
  sizeOption,
28248
28656
  `${mainPaneSize}%`
28249
- ], {
28250
- stdout: "pipe",
28251
- stderr: "pipe"
28252
- });
28253
- await sizeProc.exited;
28254
- const reapplyProc = crossSpawn([tmux, "select-layout", ...this.targetArgs(), layout], {
28255
- stdout: "pipe",
28256
- stderr: "pipe"
28257
- });
28258
- 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;
28259
28663
  }
28260
28664
  log("[tmux] applyLayout: applied", { layout, mainPaneSize });
28261
28665
  } catch (err) {
28262
28666
  log("[tmux] applyLayout: exception", { error: String(err) });
28263
28667
  }
28264
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
+ }
28265
28689
  async getBinary() {
28266
28690
  await this.isAvailable();
28267
28691
  return this.binaryPath;
@@ -28283,23 +28707,23 @@ class TmuxMultiplexer {
28283
28707
  return null;
28284
28708
  }
28285
28709
  const stdout = await proc.stdout();
28286
- const path13 = stdout.trim().split(`
28710
+ const path14 = stdout.trim().split(`
28287
28711
  `)[0];
28288
- if (!path13) {
28712
+ if (!path14) {
28289
28713
  log("[tmux] findBinary: no path in output");
28290
28714
  return null;
28291
28715
  }
28292
- const verifyProc = crossSpawn([path13, "-V"], {
28716
+ const verifyProc = crossSpawn([path14, "-V"], {
28293
28717
  stdout: "pipe",
28294
28718
  stderr: "pipe"
28295
28719
  });
28296
28720
  const verifyExit = await verifyProc.exited;
28297
28721
  if (verifyExit !== 0) {
28298
- log("[tmux] findBinary: tmux -V failed", { path: path13, verifyExit });
28722
+ log("[tmux] findBinary: tmux -V failed", { path: path14, verifyExit });
28299
28723
  return null;
28300
28724
  }
28301
- log("[tmux] findBinary: found", { path: path13 });
28302
- return path13;
28725
+ log("[tmux] findBinary: found", { path: path14 });
28726
+ return path14;
28303
28727
  } catch (err) {
28304
28728
  log("[tmux] findBinary: exception", { error: String(err) });
28305
28729
  return null;
@@ -29010,17 +29434,17 @@ async function isServerRunning(serverUrl, timeoutMs = 3000, maxAttempts = 2) {
29010
29434
  import { tool as tool2 } from "@opencode-ai/plugin/tool";
29011
29435
 
29012
29436
  // src/tools/ast-grep/cli.ts
29013
- import { existsSync as existsSync8 } from "node:fs";
29437
+ import { existsSync as existsSync9 } from "node:fs";
29014
29438
 
29015
29439
  // src/tools/ast-grep/constants.ts
29016
- import { existsSync as existsSync7, statSync as statSync4 } from "node:fs";
29440
+ import { existsSync as existsSync8, statSync as statSync4 } from "node:fs";
29017
29441
  import { createRequire as createRequire3 } from "node:module";
29018
29442
  import { dirname as dirname6, join as join11 } from "node:path";
29019
29443
 
29020
29444
  // src/tools/ast-grep/downloader.ts
29021
- 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";
29022
29446
  import { createRequire as createRequire2 } from "node:module";
29023
- import { homedir as homedir4 } from "node:os";
29447
+ import { homedir as homedir5 } from "node:os";
29024
29448
  import { join as join10 } from "node:path";
29025
29449
  var REPO = "ast-grep/ast-grep";
29026
29450
  var DEFAULT_VERSION = "0.40.0";
@@ -29045,11 +29469,11 @@ var PLATFORM_MAP = {
29045
29469
  function getCacheDir2() {
29046
29470
  if (process.platform === "win32") {
29047
29471
  const localAppData = process.env.LOCALAPPDATA || process.env.APPDATA;
29048
- const base2 = localAppData || join10(homedir4(), "AppData", "Local");
29472
+ const base2 = localAppData || join10(homedir5(), "AppData", "Local");
29049
29473
  return join10(base2, "oh-my-opencode-slim", "bin");
29050
29474
  }
29051
29475
  const xdgCache = process.env.XDG_CACHE_HOME;
29052
- const base = xdgCache || join10(homedir4(), ".cache");
29476
+ const base = xdgCache || join10(homedir5(), ".cache");
29053
29477
  return join10(base, "oh-my-opencode-slim", "bin");
29054
29478
  }
29055
29479
  function getBinaryName() {
@@ -29057,7 +29481,7 @@ function getBinaryName() {
29057
29481
  }
29058
29482
  function getCachedBinaryPath() {
29059
29483
  const binaryPath = join10(getCacheDir2(), getBinaryName());
29060
- return existsSync6(binaryPath) ? binaryPath : null;
29484
+ return existsSync7(binaryPath) ? binaryPath : null;
29061
29485
  }
29062
29486
  async function downloadAstGrep(version = DEFAULT_VERSION) {
29063
29487
  const platformKey = `${process.platform}-${process.arch}`;
@@ -29069,16 +29493,16 @@ async function downloadAstGrep(version = DEFAULT_VERSION) {
29069
29493
  const cacheDir = getCacheDir2();
29070
29494
  const binaryName = getBinaryName();
29071
29495
  const binaryPath = join10(cacheDir, binaryName);
29072
- if (existsSync6(binaryPath)) {
29496
+ if (existsSync7(binaryPath)) {
29073
29497
  return binaryPath;
29074
29498
  }
29075
- const { arch, os: os4 } = platformInfo;
29076
- const assetName = `app-${arch}-${os4}.zip`;
29499
+ const { arch, os: os5 } = platformInfo;
29500
+ const assetName = `app-${arch}-${os5}.zip`;
29077
29501
  const downloadUrl = `https://github.com/${REPO}/releases/download/${version}/${assetName}`;
29078
29502
  console.log(`[oh-my-opencode-slim] Downloading ast-grep binary...`);
29079
29503
  try {
29080
- if (!existsSync6(cacheDir)) {
29081
- mkdirSync4(cacheDir, { recursive: true });
29504
+ if (!existsSync7(cacheDir)) {
29505
+ mkdirSync5(cacheDir, { recursive: true });
29082
29506
  }
29083
29507
  const response = await fetch(downloadUrl, { redirect: "follow" });
29084
29508
  if (!response.ok) {
@@ -29088,10 +29512,10 @@ async function downloadAstGrep(version = DEFAULT_VERSION) {
29088
29512
  const arrayBuffer = await response.arrayBuffer();
29089
29513
  await crossWrite(archivePath, arrayBuffer);
29090
29514
  await extractZip(archivePath, cacheDir);
29091
- if (existsSync6(archivePath)) {
29515
+ if (existsSync7(archivePath)) {
29092
29516
  unlinkSync4(archivePath);
29093
29517
  }
29094
- if (process.platform !== "win32" && existsSync6(binaryPath)) {
29518
+ if (process.platform !== "win32" && existsSync7(binaryPath)) {
29095
29519
  chmodSync(binaryPath, 493);
29096
29520
  }
29097
29521
  console.log(`[oh-my-opencode-slim] ast-grep binary ready.`);
@@ -29174,7 +29598,7 @@ function findSgCliPathSync() {
29174
29598
  const cliPkgPath = require2.resolve("@ast-grep/cli/package.json");
29175
29599
  const cliDir = dirname6(cliPkgPath);
29176
29600
  const sgPath = join11(cliDir, binaryName);
29177
- if (existsSync7(sgPath) && isValidBinary(sgPath)) {
29601
+ if (existsSync8(sgPath) && isValidBinary(sgPath)) {
29178
29602
  return sgPath;
29179
29603
  }
29180
29604
  } catch {}
@@ -29186,16 +29610,16 @@ function findSgCliPathSync() {
29186
29610
  const pkgDir = dirname6(pkgPath);
29187
29611
  const astGrepName = process.platform === "win32" ? "ast-grep.exe" : "ast-grep";
29188
29612
  const binaryPath = join11(pkgDir, astGrepName);
29189
- if (existsSync7(binaryPath) && isValidBinary(binaryPath)) {
29613
+ if (existsSync8(binaryPath) && isValidBinary(binaryPath)) {
29190
29614
  return binaryPath;
29191
29615
  }
29192
29616
  } catch {}
29193
29617
  }
29194
29618
  if (process.platform === "darwin") {
29195
29619
  const homebrewPaths = ["/opt/homebrew/bin/sg", "/usr/local/bin/sg"];
29196
- for (const path13 of homebrewPaths) {
29197
- if (existsSync7(path13) && isValidBinary(path13)) {
29198
- return path13;
29620
+ for (const path14 of homebrewPaths) {
29621
+ if (existsSync8(path14) && isValidBinary(path14)) {
29622
+ return path14;
29199
29623
  }
29200
29624
  }
29201
29625
  }
@@ -29212,8 +29636,8 @@ function getSgCliPath() {
29212
29636
  }
29213
29637
  return "sg";
29214
29638
  }
29215
- function setSgCliPath(path13) {
29216
- resolvedCliPath = path13;
29639
+ function setSgCliPath(path14) {
29640
+ resolvedCliPath = path14;
29217
29641
  }
29218
29642
  var DEFAULT_TIMEOUT_MS2 = 300000;
29219
29643
  var DEFAULT_MAX_OUTPUT_BYTES = 1 * 1024 * 1024;
@@ -29223,7 +29647,7 @@ var DEFAULT_MAX_MATCHES = 500;
29223
29647
  var initPromise = null;
29224
29648
  async function getAstGrepPath() {
29225
29649
  const currentPath = getSgCliPath();
29226
- if (currentPath !== "sg" && existsSync8(currentPath)) {
29650
+ if (currentPath !== "sg" && existsSync9(currentPath)) {
29227
29651
  return currentPath;
29228
29652
  }
29229
29653
  if (initPromise) {
@@ -29231,7 +29655,7 @@ async function getAstGrepPath() {
29231
29655
  }
29232
29656
  initPromise = (async () => {
29233
29657
  const syncPath = findSgCliPathSync();
29234
- if (syncPath && existsSync8(syncPath)) {
29658
+ if (syncPath && existsSync9(syncPath)) {
29235
29659
  setSgCliPath(syncPath);
29236
29660
  return syncPath;
29237
29661
  }
@@ -29270,7 +29694,7 @@ async function runSg(options) {
29270
29694
  const paths2 = options.paths && options.paths.length > 0 ? options.paths : ["."];
29271
29695
  args.push(...paths2);
29272
29696
  let cliPath = getSgCliPath();
29273
- if (!existsSync8(cliPath) && cliPath !== "sg") {
29697
+ if (!existsSync9(cliPath) && cliPath !== "sg") {
29274
29698
  const downloadedPath = await getAstGrepPath();
29275
29699
  if (downloadedPath) {
29276
29700
  cliPath = downloadedPath;
@@ -29620,15 +30044,15 @@ Returns the councillor responses with a summary footer.`,
29620
30044
  }
29621
30045
  // src/tui-state.ts
29622
30046
  import * as fs9 from "node:fs";
29623
- import * as os4 from "node:os";
29624
- import * as path13 from "node:path";
30047
+ import * as os5 from "node:os";
30048
+ import * as path14 from "node:path";
29625
30049
  var STATE_DIR = "oh-my-opencode-slim";
29626
30050
  var STATE_FILE = "tui-state.json";
29627
30051
  function dataDir() {
29628
- return process.env.XDG_DATA_HOME ?? path13.join(os4.homedir(), ".local", "share");
30052
+ return process.env.XDG_DATA_HOME ?? path14.join(os5.homedir(), ".local", "share");
29629
30053
  }
29630
30054
  function getTuiStatePath() {
29631
- return path13.join(dataDir(), "opencode", "storage", STATE_DIR, STATE_FILE);
30055
+ return path14.join(dataDir(), "opencode", "storage", STATE_DIR, STATE_FILE);
29632
30056
  }
29633
30057
  function emptySnapshot() {
29634
30058
  return {
@@ -29664,7 +30088,7 @@ async function readTuiSnapshotAsync() {
29664
30088
  function writeTuiSnapshot(snapshot) {
29665
30089
  try {
29666
30090
  const filePath = getTuiStatePath();
29667
- fs9.mkdirSync(path13.dirname(filePath), { recursive: true });
30091
+ fs9.mkdirSync(path14.dirname(filePath), { recursive: true });
29668
30092
  fs9.writeFileSync(filePath, `${JSON.stringify(snapshot)}
29669
30093
  `);
29670
30094
  } catch {}
@@ -29880,15 +30304,15 @@ var BINARY_PREFIXES = [
29880
30304
  ];
29881
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.";
29882
30306
  // src/tools/smartfetch/tool.ts
29883
- import os5 from "node:os";
29884
- import path17 from "node:path";
30307
+ import os6 from "node:os";
30308
+ import path18 from "node:path";
29885
30309
  import {
29886
30310
  tool as tool4
29887
30311
  } from "@opencode-ai/plugin";
29888
30312
 
29889
30313
  // src/tools/smartfetch/binary.ts
29890
30314
  import { mkdir as mkdir2, writeFile as writeFile2 } from "node:fs/promises";
29891
- import path14 from "node:path";
30315
+ import path15 from "node:path";
29892
30316
  function extensionForMime(contentType) {
29893
30317
  const mime = contentType.split(";")[0]?.trim().toLowerCase();
29894
30318
  const map = {
@@ -29909,10 +30333,10 @@ function buildBinaryResultMessage(fetchResult, savedPath) {
29909
30333
  async function saveBinary(binaryDir, data, contentType, filename) {
29910
30334
  await mkdir2(binaryDir, { recursive: true });
29911
30335
  const initialName = filename || `webfetch-${Date.now()}.${extensionForMime(contentType)}`;
29912
- const parsed = path14.parse(initialName);
30336
+ const parsed = path15.parse(initialName);
29913
30337
  for (let attempt = 0;attempt < 1000; attempt++) {
29914
30338
  const candidateName = attempt === 0 ? initialName : `${parsed.name}-${attempt}${parsed.ext || `.${extensionForMime(contentType)}`}`;
29915
- const file = path14.join(binaryDir, candidateName);
30339
+ const file = path15.join(binaryDir, candidateName);
29916
30340
  try {
29917
30341
  await writeFile2(file, data, { flag: "wx" });
29918
30342
  return file;
@@ -30566,7 +30990,7 @@ var L = class u2 {
30566
30990
  };
30567
30991
 
30568
30992
  // src/tools/smartfetch/network.ts
30569
- import path15 from "node:path";
30993
+ import path16 from "node:path";
30570
30994
 
30571
30995
  // src/tools/smartfetch/utils.ts
30572
30996
  var import_readability = __toESM(require_readability(), 1);
@@ -31291,7 +31715,7 @@ function inferFilenameFromUrl(url) {
31291
31715
  function truncateFilename(name, maxLength = 180) {
31292
31716
  if (name.length <= maxLength)
31293
31717
  return name;
31294
- const parsed = path15.parse(name);
31718
+ const parsed = path16.parse(name);
31295
31719
  const ext = parsed.ext || "";
31296
31720
  const baseLimit = Math.max(1, maxLength - ext.length);
31297
31721
  return `${parsed.name.slice(0, baseLimit)}${ext}`;
@@ -31461,9 +31885,9 @@ function isInvalidLlmsResult(fetchResult) {
31461
31885
  }
31462
31886
 
31463
31887
  // src/tools/smartfetch/secondary-model.ts
31464
- import { existsSync as existsSync9 } from "node:fs";
31888
+ import { existsSync as existsSync10 } from "node:fs";
31465
31889
  import { readFile as readFile4 } from "node:fs/promises";
31466
- import path16 from "node:path";
31890
+ import path17 from "node:path";
31467
31891
  function parseModelRef(value) {
31468
31892
  if (!value)
31469
31893
  return;
@@ -31489,8 +31913,8 @@ function pickAgentModelRef(value) {
31489
31913
  }
31490
31914
  function findPreferredOpenCodeConfigPath(baseDir) {
31491
31915
  for (const file of ["opencode.jsonc", "opencode.json"]) {
31492
- const fullPath = path16.join(baseDir, file);
31493
- if (existsSync9(fullPath))
31916
+ const fullPath = path17.join(baseDir, file);
31917
+ if (existsSync10(fullPath))
31494
31918
  return fullPath;
31495
31919
  }
31496
31920
  return;
@@ -31506,7 +31930,7 @@ async function readOpenCodeConfigFile(configPath) {
31506
31930
  }
31507
31931
  }
31508
31932
  async function readEffectiveOpenCodeConfig(directory) {
31509
- const projectDir = path16.join(directory, ".opencode");
31933
+ const projectDir = path17.join(directory, ".opencode");
31510
31934
  const userDirs = getConfigSearchDirs();
31511
31935
  const projectPath = findPreferredOpenCodeConfigPath(projectDir);
31512
31936
  const userPath = userDirs.map((configDir) => findPreferredOpenCodeConfigPath(configDir)).find(Boolean);
@@ -31667,7 +32091,7 @@ async function runSecondaryModelWithFallback(client, directory, models, prompt,
31667
32091
  // src/tools/smartfetch/tool.ts
31668
32092
  var z5 = tool4.schema;
31669
32093
  function createWebfetchTool(pluginCtx, options = {}) {
31670
- const binaryDir = options.binaryDir || path17.join(os5.tmpdir(), "opencode-smartfetch");
32094
+ const binaryDir = options.binaryDir || path18.join(os6.tmpdir(), "opencode-smartfetch");
31671
32095
  return tool4({
31672
32096
  description: WEBFETCH_DESCRIPTION,
31673
32097
  args: {
@@ -32263,6 +32687,7 @@ var OhMyOpenCodeLite = async (ctx) => {
32263
32687
  let taskSessionManagerHook;
32264
32688
  let interviewManager;
32265
32689
  let presetManager;
32690
+ let divoomManager;
32266
32691
  let councilTools;
32267
32692
  let webfetch;
32268
32693
  let rewriteDisplayNameMentions;
@@ -32357,6 +32782,7 @@ var OhMyOpenCodeLite = async (ctx) => {
32357
32782
  });
32358
32783
  interviewManager = createInterviewManager(ctx, config);
32359
32784
  presetManager = createPresetManager(ctx, config);
32785
+ divoomManager = createDivoomManager(config.divoom);
32360
32786
  toolCount = Object.keys(councilTools).length + Object.keys(todoContinuationHook.tool).length + 1 + 2;
32361
32787
  } catch (err) {
32362
32788
  log("[plugin] FATAL: init failed", String(err));
@@ -32393,6 +32819,7 @@ var OhMyOpenCodeLite = async (ctx) => {
32393
32819
  appLog(ctx, "warn", msg).catch(() => {});
32394
32820
  }
32395
32821
  });
32822
+ divoomManager.onPluginLoad();
32396
32823
  return {
32397
32824
  name: "oh-my-opencode-slim",
32398
32825
  agent: agents,
@@ -32625,6 +33052,37 @@ var OhMyOpenCodeLite = async (ctx) => {
32625
33052
  await multiplexerSessionManager.onSessionDeleted(event);
32626
33053
  await interviewManager.handleEvent(input);
32627
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
+ }
32628
33086
  if (input.event.type === "session.deleted") {
32629
33087
  const props = input.event.properties;
32630
33088
  const sessionID = props?.info?.id ?? props?.sessionID;
@@ -32639,6 +33097,13 @@ var OhMyOpenCodeLite = async (ctx) => {
32639
33097
  "tool.execute.before": async (input, output) => {
32640
33098
  await applyPatchHook["tool.execute.before"](input, output);
32641
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
+ }
32642
33107
  },
32643
33108
  "command.execute.before": async (input, output) => {
32644
33109
  await todoContinuationHook.handleCommandExecuteBefore(input, output);
@@ -32701,11 +33166,31 @@ ${output.system[0]}` : "");
32701
33166
  await filterAvailableSkillsHook["experimental.chat.messages.transform"](input, typedOutput);
32702
33167
  },
32703
33168
  "tool.execute.after": async (input, output) => {
32704
- await delegateTaskRetryHook["tool.execute.after"](input, output);
32705
- await jsonErrorRecoveryHook["tool.execute.after"](input, output);
32706
- await todoContinuationHook.handleToolExecuteAfter(input, output);
32707
- await postFileToolNudgeHook["tool.execute.after"](input, output);
32708
- 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
+ }
32709
33194
  }
32710
33195
  };
32711
33196
  };