@runfusion/fusion 0.1.3 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/dist/bin.js +2113 -1017
  2. package/dist/client/assets/AgentDetailView-CDZED6Dy.css +1 -0
  3. package/dist/client/assets/AgentDetailView-zycSdnO8.js +28 -0
  4. package/dist/client/assets/AgentsView-DoQkkDLf.css +1 -0
  5. package/dist/client/assets/AgentsView-pO7WiBS5.js +522 -0
  6. package/dist/client/assets/ChatView-BOd-sxbT.js +1 -0
  7. package/dist/client/assets/DevServerView-09GQf34f.js +11 -0
  8. package/dist/client/assets/DevServerView-ZeBGQkLI.css +1 -0
  9. package/dist/client/assets/DirectoryPicker-CcdN1Zs7.js +1 -0
  10. package/dist/client/assets/DocumentsView-CS8aiwtz.js +1 -0
  11. package/dist/client/assets/DocumentsView-Co9to4Zp.css +1 -0
  12. package/dist/client/assets/InsightsView-Bu9Cv8Ol.js +11 -0
  13. package/dist/client/assets/InsightsView-Egu71gmh.css +1 -0
  14. package/dist/client/assets/MemoryView-CtqgDtV9.js +2 -0
  15. package/dist/client/assets/MemoryView-DhinauGs.css +1 -0
  16. package/dist/client/assets/NodesView-BInPcedy.js +14 -0
  17. package/dist/client/assets/NodesView-DlQZHGXA.css +1 -0
  18. package/dist/client/assets/PiExtensionsManager-COxkYM2m.js +11 -0
  19. package/dist/client/assets/PiExtensionsManager-CPgmJgDk.css +1 -0
  20. package/dist/client/assets/PluginManager-CXUWZBOc.js +1 -0
  21. package/dist/client/assets/PluginManager-D64RIzmL.css +1 -0
  22. package/dist/client/assets/RoadmapsView-BOYnyMCh.css +1 -0
  23. package/dist/client/assets/RoadmapsView-BbCexaoi.js +6 -0
  24. package/dist/client/assets/SetupWizardModal-Cakxqkad.js +1 -0
  25. package/dist/client/assets/SkillsView-Cytf009Z.css +1 -0
  26. package/dist/client/assets/SkillsView-D3iqYCVf.js +1 -0
  27. package/dist/client/assets/folder-open-kO5Hsk66.js +6 -0
  28. package/dist/client/assets/index-BiSuUXCa.css +1 -0
  29. package/dist/client/assets/index-y194HxzU.js +644 -0
  30. package/dist/client/assets/upload-DHBQat92.js +6 -0
  31. package/dist/client/index.html +2 -2
  32. package/dist/extension.js +175 -66
  33. package/dist/pi-claude-cli/src/__tests__/control-handler.test.ts +191 -0
  34. package/dist/pi-claude-cli/src/__tests__/event-bridge.test.ts +1244 -0
  35. package/dist/pi-claude-cli/src/__tests__/mcp-config.test.ts +272 -0
  36. package/dist/pi-claude-cli/src/__tests__/process-manager.test.ts +619 -0
  37. package/dist/pi-claude-cli/src/__tests__/prompt-builder.test.ts +1067 -0
  38. package/dist/pi-claude-cli/src/__tests__/provider.test.ts +1902 -0
  39. package/dist/pi-claude-cli/src/__tests__/stream-parser.test.ts +188 -0
  40. package/dist/pi-claude-cli/src/__tests__/thinking-config.test.ts +141 -0
  41. package/dist/pi-claude-cli/src/__tests__/tool-mapping.test.ts +252 -0
  42. package/package.json +11 -5
  43. package/dist/client/assets/index-BuenKJX0.css +0 -1
  44. package/dist/client/assets/index-CjGu8HRV.js +0 -1250
package/dist/bin.js CHANGED
@@ -61,6 +61,7 @@ var init_settings_schema = __esm({
61
61
  defaultThinkingLevel: void 0,
62
62
  ntfyEnabled: false,
63
63
  ntfyTopic: void 0,
64
+ ntfyBaseUrl: void 0,
64
65
  ntfyEvents: ["in-review", "merged", "failed", "awaiting-approval", "awaiting-user-review", "planning-awaiting-input"],
65
66
  ntfyDashboardHost: void 0,
66
67
  defaultProjectId: void 0,
@@ -18902,7 +18903,7 @@ var require_luxon = __commonJS({
18902
18903
  if (this.rtf) {
18903
18904
  return this.rtf.format(count, unit);
18904
18905
  } else {
18905
- return formatRelativeTime(unit, count, this.opts.numeric, this.opts.style !== "long");
18906
+ return formatRelativeTime2(unit, count, this.opts.numeric, this.opts.style !== "long");
18906
18907
  }
18907
18908
  }
18908
18909
  formatToParts(count, unit) {
@@ -19993,7 +19994,7 @@ var require_luxon = __commonJS({
19993
19994
  function eraForDateTime(dt, length) {
19994
19995
  return eras(length)[dt.year < 0 ? 0 : 1];
19995
19996
  }
19996
- function formatRelativeTime(unit, count, numeric = "always", narrow = false) {
19997
+ function formatRelativeTime2(unit, count, numeric = "always", narrow = false) {
19997
19998
  const units = {
19998
19999
  years: ["year", "yr."],
19999
20000
  quarters: ["quarter", "qtr."],
@@ -51446,10 +51447,10 @@ var require_lib = __commonJS({
51446
51447
  } else {
51447
51448
  terminalCtor = require_unixTerminal().UnixTerminal;
51448
51449
  }
51449
- function spawn7(file, args, opt) {
51450
+ function spawn8(file, args, opt) {
51450
51451
  return new terminalCtor(file, args, opt);
51451
51452
  }
51452
- exports.spawn = spawn7;
51453
+ exports.spawn = spawn8;
51453
51454
  function fork2(file, args, opt) {
51454
51455
  return new terminalCtor(file, args, opt);
51455
51456
  }
@@ -59779,7 +59780,7 @@ var init_roadmap_routes = __esm({
59779
59780
  }
59780
59781
  });
59781
59782
 
59782
- // ../engine/src/logger.js
59783
+ // ../engine/src/logger.ts
59783
59784
  function withSeverityMarker2(level, payload) {
59784
59785
  return `${LOG_LEVEL_MARKER_PREFIX2}${level}${LOG_LEVEL_MARKER_SUFFIX2}${payload}`;
59785
59786
  }
@@ -59787,13 +59788,13 @@ function createLogger2(prefix) {
59787
59788
  const tag = `[${prefix}]`;
59788
59789
  return {
59789
59790
  log(message, ...args) {
59790
- globalThis.console.error(withSeverityMarker2("info", `${tag} ${message}`), ...args);
59791
+ console.error(withSeverityMarker2("info", `${tag} ${message}`), ...args);
59791
59792
  },
59792
59793
  warn(message, ...args) {
59793
- globalThis.console.warn(withSeverityMarker2("warn", `${tag} ${message}`), ...args);
59794
+ console.warn(withSeverityMarker2("warn", `${tag} ${message}`), ...args);
59794
59795
  },
59795
59796
  error(message, ...args) {
59796
- globalThis.console.error(withSeverityMarker2("error", `${tag} ${message}`), ...args);
59797
+ console.error(withSeverityMarker2("error", `${tag} ${message}`), ...args);
59797
59798
  }
59798
59799
  };
59799
59800
  }
@@ -59819,7 +59820,7 @@ ${stack}` : message2;
59819
59820
  }
59820
59821
  var LOG_LEVEL_MARKER_PREFIX2, LOG_LEVEL_MARKER_SUFFIX2, schedulerLog, executorLog, triageLog, piLog, extensionsLog, mergerLog, worktreePoolLog, reviewerLog, prMonitorLog, runtimeLog, ipcLog, projectManagerLog, hybridExecutorLog, autopilotLog, heartbeatLog, remoteNodeLog, nodeHealthMonitorLog, peerExchangeLog;
59821
59822
  var init_logger2 = __esm({
59822
- "../engine/src/logger.js"() {
59823
+ "../engine/src/logger.ts"() {
59823
59824
  "use strict";
59824
59825
  LOG_LEVEL_MARKER_PREFIX2 = "\0fnlvl=";
59825
59826
  LOG_LEVEL_MARKER_SUFFIX2 = "\0";
@@ -60978,7 +60979,7 @@ var init_concurrency = __esm({
60978
60979
  }
60979
60980
  });
60980
60981
 
60981
- // ../engine/src/skill-resolver.js
60982
+ // ../engine/src/skill-resolver.ts
60982
60983
  import { existsSync as existsSync19, readFileSync as readFileSync5 } from "node:fs";
60983
60984
  import { join as join25 } from "node:path";
60984
60985
  function readJsonObject(path4) {
@@ -61102,9 +61103,13 @@ function createSkillsOverrideFromSelection(selection, options = {}) {
61102
61103
  let filteredSkills;
61103
61104
  if (hasRequestedNames) {
61104
61105
  const requestedNamesLower = new Set(requestedSkillNames.map((n) => n.toLowerCase()));
61105
- filteredSkills = base.skills.filter((skill) => requestedNamesLower.has(skill.name.toLowerCase()) && !excludedSkillPaths.has(skill.filePath));
61106
+ filteredSkills = base.skills.filter(
61107
+ (skill) => requestedNamesLower.has(skill.name.toLowerCase()) && !excludedSkillPaths.has(skill.filePath)
61108
+ );
61106
61109
  } else if (hasPatterns) {
61107
- filteredSkills = base.skills.filter((skill) => allowedSkillPaths.has(skill.filePath) && !excludedSkillPaths.has(skill.filePath));
61110
+ filteredSkills = base.skills.filter(
61111
+ (skill) => allowedSkillPaths.has(skill.filePath) && !excludedSkillPaths.has(skill.filePath)
61112
+ );
61108
61113
  } else if (hasExcluded) {
61109
61114
  filteredSkills = base.skills.filter((skill) => !excludedSkillPaths.has(skill.filePath));
61110
61115
  } else {
@@ -61144,6 +61149,7 @@ function createSkillsOverrideFromSelection(selection, options = {}) {
61144
61149
  }
61145
61150
  }
61146
61151
  if (newDiagnostics.length > 0) {
61152
+ const _purpose = sessionPurpose ? `[${sessionPurpose}]` : "skills";
61147
61153
  for (const diag of newDiagnostics) {
61148
61154
  piLog.warn(`[skills] ${diag.type}: ${diag.message}`);
61149
61155
  }
@@ -61155,21 +61161,20 @@ function createSkillsOverrideFromSelection(selection, options = {}) {
61155
61161
  };
61156
61162
  }
61157
61163
  var init_skill_resolver = __esm({
61158
- "../engine/src/skill-resolver.js"() {
61164
+ "../engine/src/skill-resolver.ts"() {
61159
61165
  "use strict";
61160
61166
  init_logger2();
61161
61167
  }
61162
61168
  });
61163
61169
 
61164
- // ../engine/src/context-limit-detector.js
61170
+ // ../engine/src/context-limit-detector.ts
61165
61171
  function isContextLimitError(message) {
61166
- if (!message)
61167
- return false;
61172
+ if (!message) return false;
61168
61173
  return CONTEXT_OVERFLOW_PATTERNS.some((pattern) => pattern.test(message));
61169
61174
  }
61170
61175
  var CONTEXT_OVERFLOW_PATTERNS;
61171
61176
  var init_context_limit_detector = __esm({
61172
- "../engine/src/context-limit-detector.js"() {
61177
+ "../engine/src/context-limit-detector.ts"() {
61173
61178
  "use strict";
61174
61179
  CONTEXT_OVERFLOW_PATTERNS = [
61175
61180
  // Anthropic: "prompt is too long: X tokens > Y maximum"
@@ -61206,14 +61211,14 @@ var init_context_limit_detector = __esm({
61206
61211
  }
61207
61212
  });
61208
61213
 
61209
- // ../engine/src/auth-storage.js
61214
+ // ../engine/src/auth-storage.ts
61210
61215
  import { existsSync as existsSync20, readFileSync as readFileSync6 } from "node:fs";
61211
61216
  import { homedir as homedir5 } from "node:os";
61212
61217
  import { join as join26 } from "node:path";
61213
61218
  import { AuthStorage } from "@mariozechner/pi-coding-agent";
61214
61219
  import { getOAuthProvider } from "@mariozechner/pi-ai/oauth";
61215
61220
  function getHomeDir2() {
61216
- return globalThis.process.env.HOME || globalThis.process.env.USERPROFILE || homedir5();
61221
+ return process.env.HOME || process.env.USERPROFILE || homedir5();
61217
61222
  }
61218
61223
  function getFusionAuthPath2(home = getHomeDir2()) {
61219
61224
  return join26(home, ".fusion", "agent", "auth.json");
@@ -61257,9 +61262,8 @@ function readLegacyCredentials(authPaths = getLegacyAuthPaths()) {
61257
61262
  return credentials;
61258
61263
  }
61259
61264
  function resolveStoredApiKey(key) {
61260
- if (!key)
61261
- return void 0;
61262
- return globalThis.process.env[key] ?? key;
61265
+ if (!key) return void 0;
61266
+ return process.env[key] ?? key;
61263
61267
  }
61264
61268
  function resolveOAuthApiKey(providerId, credential) {
61265
61269
  if (credential.type !== "oauth" || typeof credential.access !== "string" || typeof credential.refresh !== "string" || typeof credential.expires !== "number" || Date.now() >= credential.expires) {
@@ -61305,8 +61309,7 @@ function createFusionAuthStorage() {
61305
61309
  if (prop === "getApiKey") {
61306
61310
  return async (provider) => {
61307
61311
  const primaryKey = await target.getApiKey(provider);
61308
- if (primaryKey)
61309
- return primaryKey;
61312
+ if (primaryKey) return primaryKey;
61310
61313
  return resolveStoredCredentialApiKey(provider, legacyCredentials[provider]);
61311
61314
  };
61312
61315
  }
@@ -61315,12 +61318,12 @@ function createFusionAuthStorage() {
61315
61318
  });
61316
61319
  }
61317
61320
  var init_auth_storage = __esm({
61318
- "../engine/src/auth-storage.js"() {
61321
+ "../engine/src/auth-storage.ts"() {
61319
61322
  "use strict";
61320
61323
  }
61321
61324
  });
61322
61325
 
61323
- // ../engine/src/pi.js
61326
+ // ../engine/src/pi.ts
61324
61327
  var pi_exports = {};
61325
61328
  __export(pi_exports, {
61326
61329
  COMPACTION_FALLBACK_INSTRUCTIONS: () => COMPACTION_FALLBACK_INSTRUCTIONS,
@@ -61334,19 +61337,35 @@ import { existsSync as existsSync21, readFileSync as readFileSync7 } from "node:
61334
61337
  import { exec } from "node:child_process";
61335
61338
  import { promisify as promisify2 } from "node:util";
61336
61339
  import { basename as basename9, dirname as dirname9, join as join27, relative as relative4, isAbsolute as isAbsolute6, resolve as resolve12 } from "node:path";
61337
- import { createAgentSession, createCodingTools, createExtensionRuntime, createReadOnlyTools, DefaultResourceLoader, DefaultPackageManager, discoverAndLoadExtensions, ModelRegistry, SessionManager, SettingsManager } from "@mariozechner/pi-coding-agent";
61340
+ import {
61341
+ createAgentSession,
61342
+ createCodingTools,
61343
+ createExtensionRuntime,
61344
+ createReadOnlyTools,
61345
+ DefaultResourceLoader,
61346
+ DefaultPackageManager,
61347
+ discoverAndLoadExtensions,
61348
+ ModelRegistry,
61349
+ SessionManager,
61350
+ SettingsManager
61351
+ } from "@mariozechner/pi-coding-agent";
61338
61352
  function getSessionStateError(session) {
61339
- const error = session.state?.error;
61353
+ const state = session.state;
61354
+ const error = state?.errorMessage ?? state?.error;
61340
61355
  return typeof error === "string" ? error : "";
61341
61356
  }
61342
61357
  function clearSessionStateError(session) {
61343
61358
  const state = session.state;
61344
- if (!state || typeof state !== "object" || !("error" in state)) {
61359
+ if (!state || typeof state !== "object") {
61345
61360
  return;
61346
61361
  }
61347
- try {
61348
- state.error = void 0;
61349
- } catch {
61362
+ for (const key of ["errorMessage", "error"]) {
61363
+ if (key in state) {
61364
+ try {
61365
+ state[key] = void 0;
61366
+ } catch {
61367
+ }
61368
+ }
61350
61369
  }
61351
61370
  }
61352
61371
  async function promptSessionAndCheck(session, prompt, options) {
@@ -61358,6 +61377,29 @@ async function promptSessionAndCheck(session, prompt, options) {
61358
61377
  }
61359
61378
  const stateError = getSessionStateError(session);
61360
61379
  if (stateError) {
61380
+ if (/Cannot read propert(y|ies) of (undefined|null)/i.test(stateError)) {
61381
+ try {
61382
+ const messages = session.agent?.state?.messages ?? session.state?.messages;
61383
+ if (Array.isArray(messages)) {
61384
+ const recent = messages.slice(-6).map((m, idx) => {
61385
+ const i = messages.length - 6 + idx;
61386
+ const content = m?.content;
61387
+ return {
61388
+ index: i < 0 ? idx : i,
61389
+ role: m?.role,
61390
+ contentType: Array.isArray(content) ? `array(len=${content.length})` : typeof content,
61391
+ toolName: m.toolName,
61392
+ stopReason: m.stopReason
61393
+ };
61394
+ });
61395
+ piLog.error(`pi state error \u2014 transcript tail (${messages.length} msgs total): ${JSON.stringify(recent)}`);
61396
+ } else {
61397
+ piLog.error(`pi state error \u2014 state.messages is not an array: ${typeof messages}`);
61398
+ }
61399
+ } catch (inspectErr) {
61400
+ piLog.warn(`pi state error \u2014 failed to inspect transcript: ${inspectErr instanceof Error ? inspectErr.message : String(inspectErr)}`);
61401
+ }
61402
+ }
61361
61403
  throw new Error(stateError);
61362
61404
  }
61363
61405
  }
@@ -61409,8 +61451,7 @@ async function promptWithFallback(session, prompt, options) {
61409
61451
  }
61410
61452
  function describeModel(session) {
61411
61453
  const model = session.model;
61412
- if (!model)
61413
- return "unknown model";
61454
+ if (!model) return "unknown model";
61414
61455
  return `${model.provider}/${model.id}`;
61415
61456
  }
61416
61457
  function compactMarkdownMemorySection(sectionBody) {
@@ -61463,7 +61504,9 @@ async function retryWithCompactedPromptMemory(session, prompt, options) {
61463
61504
  if (!compactedPrompt) {
61464
61505
  return { recovered: false };
61465
61506
  }
61466
- piLog.log(`promptWithFallback: retrying with compacted prompt memory (${prompt.length} \u2192 ${compactedPrompt.length} chars)`);
61507
+ piLog.log(
61508
+ `promptWithFallback: retrying with compacted prompt memory (${prompt.length} \u2192 ${compactedPrompt.length} chars)`
61509
+ );
61467
61510
  try {
61468
61511
  await promptSessionAndCheck(session, compactedPrompt, options);
61469
61512
  piLog.log("promptWithFallback: prompt completed after prompt-memory compaction");
@@ -61480,7 +61523,7 @@ async function flushMemoryBeforeSessionCompaction(session) {
61480
61523
  }
61481
61524
  const flushPrompt = [
61482
61525
  "Before context compaction, preserve only unresolved durable memory if needed.",
61483
- "If memory_append is available and you learned reusable project decisions, conventions, pitfalls, or open loops that are not already saved, append them now.",
61526
+ "If fn_memory_append is available and you learned reusable project decisions, conventions, pitfalls, or open loops that are not already saved, append them now.",
61484
61527
  'Use layer="long-term" for durable facts and layer="daily" for running notes/open loops.',
61485
61528
  "If there is nothing durable to save, reply exactly: NONE."
61486
61529
  ].join("\n");
@@ -61525,7 +61568,9 @@ function resolveConfiguredModel(modelRegistry, kind, provider, modelId) {
61525
61568
  piLog.warn(`${kind} model ${provider}/${modelId} not in registry; using provider base model as template`);
61526
61569
  return { ...baseModel, id: modelId, name: modelId };
61527
61570
  }
61528
- throw new Error(`Configured ${kind} model ${provider}/${modelId} was not found in the pi model registry. Open Settings and choose a model from /api/models, or update your pi model configuration.`);
61571
+ throw new Error(
61572
+ `Configured ${kind} model ${provider}/${modelId} was not found in the pi model registry. Open Settings and choose a model from /api/models, or update your pi model configuration.`
61573
+ );
61529
61574
  }
61530
61575
  function isRetryableModelSelectionError(message) {
61531
61576
  const normalized = message.toLowerCase();
@@ -61570,11 +61615,18 @@ function normalizeAssistantOrToolResultMessage(message) {
61570
61615
  return false;
61571
61616
  }
61572
61617
  const role = message.role;
61573
- if (role !== "assistant" && role !== "toolResult") {
61618
+ if (role !== "assistant" && role !== "toolResult" && role !== "user") {
61574
61619
  return false;
61575
61620
  }
61576
- if (!Array.isArray(message.content)) {
61577
- message.content = [];
61621
+ const obj = message;
61622
+ if (role === "user") {
61623
+ if (typeof obj.content !== "string" && !Array.isArray(obj.content)) {
61624
+ obj.content = [];
61625
+ }
61626
+ return true;
61627
+ }
61628
+ if (!Array.isArray(obj.content)) {
61629
+ obj.content = [];
61578
61630
  }
61579
61631
  return true;
61580
61632
  }
@@ -61631,6 +61683,12 @@ function installMessageContentGuard(session, sessionManager) {
61631
61683
  if (session.__fusionMessageContentGuardInstalled) {
61632
61684
  return;
61633
61685
  }
61686
+ const existingMessages = session.agent?.state?.messages;
61687
+ if (Array.isArray(existingMessages)) {
61688
+ for (const candidate of existingMessages) {
61689
+ normalizeAssistantOrToolResultMessage(candidate);
61690
+ }
61691
+ }
61634
61692
  if (typeof session.subscribe === "function") {
61635
61693
  session.subscribe((event) => {
61636
61694
  if (!event || typeof event !== "object" || event.type !== "message_end") {
@@ -61673,8 +61731,8 @@ function createReadOnlyPiSettingsView(cwd, agentDir) {
61673
61731
  const fusionProjectSettings = readJsonObject2(join27(projectRoot, ".fusion", "settings.json"));
61674
61732
  const mergedSettings = { ...globalSettings, ...fusionProjectSettings };
61675
61733
  return {
61676
- getGlobalSettings: () => globalThis.structuredClone(globalSettings),
61677
- getProjectSettings: () => globalThis.structuredClone(fusionProjectSettings),
61734
+ getGlobalSettings: () => structuredClone(globalSettings),
61735
+ getProjectSettings: () => structuredClone(fusionProjectSettings),
61678
61736
  getNpmCommand: () => Array.isArray(mergedSettings.npmCommand) ? [...mergedSettings.npmCommand] : void 0
61679
61737
  };
61680
61738
  }
@@ -61701,7 +61759,11 @@ async function registerExtensionProviders(cwd, modelRegistry) {
61701
61759
  });
61702
61760
  const resolvedPaths = await packageManager.resolve();
61703
61761
  const packageExtensionPaths = resolvedPaths.extensions.filter((resource) => resource.enabled).map((resource) => resource.path);
61704
- const extensionsResult = await discoverAndLoadExtensions([...getEnabledPiExtensionPaths(cwd), ...packageExtensionPaths], cwd, join27(resolvePiExtensionProjectRoot(cwd), ".fusion", "disabled-auto-extension-discovery"));
61762
+ const extensionsResult = await discoverAndLoadExtensions(
61763
+ [...getEnabledPiExtensionPaths(cwd), ...packageExtensionPaths],
61764
+ cwd,
61765
+ join27(resolvePiExtensionProjectRoot(cwd), ".fusion", "disabled-auto-extension-discovery")
61766
+ );
61705
61767
  for (const { path: path4, error } of extensionsResult.errors) {
61706
61768
  extensionsLog.warn(`Failed to load ${path4}: ${error}`);
61707
61769
  }
@@ -61736,7 +61798,9 @@ async function isRegisteredGitWorktree(projectRoot, worktreePath) {
61736
61798
  encoding: "utf-8"
61737
61799
  });
61738
61800
  const resolvedWorktree = resolve12(worktreePath);
61739
- return stdout.split("\n").some((line) => line.startsWith("worktree ") && resolve12(line.slice("worktree ".length)) === resolvedWorktree);
61801
+ return stdout.split("\n").some(
61802
+ (line) => line.startsWith("worktree ") && resolve12(line.slice("worktree ".length)) === resolvedWorktree
61803
+ );
61740
61804
  } catch {
61741
61805
  return false;
61742
61806
  }
@@ -61763,7 +61827,7 @@ async function assertValidWorktreeSession(cwd, projectRoot) {
61763
61827
  throw new Error(`Refusing to start coding agent in unregistered git worktree: ${cwd}`);
61764
61828
  }
61765
61829
  }
61766
- function isWorktreeAllowedPath(worktreePath, projectRoot, requestedPath) {
61830
+ function isWorktreeAllowedPath(worktreePath, projectRoot, requestedPath, toolName) {
61767
61831
  const worktreeResolved = resolve12(worktreePath);
61768
61832
  const projectRootResolved = resolve12(projectRoot);
61769
61833
  const requestedResolved = isAbsolute6(requestedPath) ? resolve12(requestedPath) : resolve12(worktreeResolved, requestedPath);
@@ -61778,8 +61842,20 @@ function isWorktreeAllowedPath(worktreePath, projectRoot, requestedPath) {
61778
61842
  if (relToProjectRoot.match(/^\.fusion\/tasks\/[^/]+\/attachments\//)) {
61779
61843
  return true;
61780
61844
  }
61845
+ const readOnlyTools = /* @__PURE__ */ new Set(["read", "glob", "grep"]);
61846
+ if (toolName && readOnlyTools.has(toolName) && /^\.fusion\/tasks\/[^/]+\/(PROMPT\.md|task\.json)$/.test(relToProjectRoot)) {
61847
+ return true;
61848
+ }
61781
61849
  return false;
61782
61850
  }
61851
+ function boundaryRejection(message) {
61852
+ return {
61853
+ content: [{ type: "text", text: message }],
61854
+ isError: true,
61855
+ ok: false,
61856
+ error: message
61857
+ };
61858
+ }
61783
61859
  function wrapToolsWithBoundary(tools, worktreePath, projectRoot) {
61784
61860
  if (!worktreePath || !projectRoot) {
61785
61861
  return tools;
@@ -61793,21 +61869,21 @@ function wrapToolsWithBoundary(tools, worktreePath, projectRoot) {
61793
61869
  return {
61794
61870
  ...tool,
61795
61871
  execute: async (...args) => {
61872
+ const _toolCallId = args[0];
61796
61873
  const params = args[1];
61874
+ const _signal = args[2];
61797
61875
  const pathArg = params.path;
61798
- if (pathArg && !isWorktreeAllowedPath(worktreePath, projectRoot, pathArg)) {
61876
+ if (pathArg && !isWorktreeAllowedPath(worktreePath, projectRoot, pathArg, tool.name)) {
61799
61877
  const relToProject = relative4(projectRoot, pathArg);
61800
- return {
61801
- ok: false,
61802
- error: `Path "${relToProject}" is outside the worktree boundary. Coding agents can only modify files inside the current worktree. Exception: .fusion/memory/ (project root) and .fusion/tasks/*/attachments/* are permitted for reading.`
61803
- };
61878
+ return boundaryRejection(
61879
+ `Path "${relToProject}" is outside the worktree boundary. Coding agents can only modify files inside the current worktree. Exceptions (read-only): .fusion/memory/, .fusion/tasks/*/attachments/, and .fusion/tasks/*/{PROMPT.md,task.json} for dependency context.`
61880
+ );
61804
61881
  }
61805
61882
  const cwdArg = params.cwd;
61806
- if (tool.name === "bash" && cwdArg && !isWorktreeAllowedPath(worktreePath, projectRoot, cwdArg)) {
61807
- return {
61808
- ok: false,
61809
- error: `Working directory is outside the worktree boundary. Commands must run inside the worktree.`
61810
- };
61883
+ if (tool.name === "bash" && cwdArg && !isWorktreeAllowedPath(worktreePath, projectRoot, cwdArg, tool.name)) {
61884
+ return boundaryRejection(
61885
+ `Working directory is outside the worktree boundary. Commands must run inside the worktree.`
61886
+ );
61811
61887
  }
61812
61888
  return originalExecute(...args);
61813
61889
  }
@@ -61817,7 +61893,7 @@ function wrapToolsWithBoundary(tools, worktreePath, projectRoot) {
61817
61893
  async function createFnAgent5(options) {
61818
61894
  piLog.log(`createFnAgent called (cwd=${options.cwd}, tools=${options.tools}, provider=${options.defaultProvider}, model=${options.defaultModelId})`);
61819
61895
  const authStorage = createFusionAuthStorage();
61820
- const modelRegistry = new ModelRegistry(authStorage, getModelRegistryModelsPath());
61896
+ const modelRegistry = ModelRegistry.create(authStorage, getModelRegistryModelsPath());
61821
61897
  await registerExtensionProviders(options.cwd, modelRegistry);
61822
61898
  const tools = options.tools === "readonly" ? createReadOnlyTools(options.cwd) : createCodingTools(options.cwd);
61823
61899
  const worktreePath = options.cwd;
@@ -61830,8 +61906,18 @@ async function createFnAgent5(options) {
61830
61906
  compaction: { enabled: true },
61831
61907
  retry: { enabled: true, maxRetries: 3 }
61832
61908
  });
61833
- const selectedModel = resolveConfiguredModel(modelRegistry, "primary", options.defaultProvider, options.defaultModelId);
61834
- const fallbackModel = resolveConfiguredModel(modelRegistry, "fallback", options.fallbackProvider, options.fallbackModelId);
61909
+ const selectedModel = resolveConfiguredModel(
61910
+ modelRegistry,
61911
+ "primary",
61912
+ options.defaultProvider,
61913
+ options.defaultModelId
61914
+ );
61915
+ const fallbackModel = resolveConfiguredModel(
61916
+ modelRegistry,
61917
+ "fallback",
61918
+ options.fallbackProvider,
61919
+ options.fallbackModelId
61920
+ );
61835
61921
  let effectiveSkillSelection = options.skillSelection;
61836
61922
  if (!effectiveSkillSelection && options.skills && options.skills.length > 0) {
61837
61923
  piLog.log(`Using skills from convenience parameter: [${options.skills.join(", ")}]`);
@@ -61857,6 +61943,7 @@ async function createFnAgent5(options) {
61857
61943
  }
61858
61944
  const resourceLoader = new DefaultResourceLoader({
61859
61945
  cwd: options.cwd,
61946
+ agentDir: getFusionAgentDir(),
61860
61947
  settingsManager,
61861
61948
  systemPromptOverride: () => options.systemPrompt,
61862
61949
  appendSystemPromptOverride: () => [],
@@ -61866,13 +61953,17 @@ async function createFnAgent5(options) {
61866
61953
  const sessionManager = options.sessionManager ?? SessionManager.inMemory();
61867
61954
  normalizeSessionHistoryEntries(sessionManager);
61868
61955
  const createSessionWithModel = async (modelOverride) => {
61956
+ const customToolList = [
61957
+ ...wrappedTools,
61958
+ ...options.customTools ?? []
61959
+ ];
61869
61960
  return createAgentSession({
61870
61961
  cwd: options.cwd,
61871
61962
  authStorage,
61872
61963
  modelRegistry,
61873
61964
  resourceLoader,
61874
- tools: wrappedTools,
61875
- customTools: options.customTools,
61965
+ noTools: "builtin",
61966
+ customTools: customToolList,
61876
61967
  sessionManager,
61877
61968
  settingsManager,
61878
61969
  ...modelOverride ? { model: modelOverride } : {}
@@ -61896,7 +61987,7 @@ async function createFnAgent5(options) {
61896
61987
  const { session } = sessionResult;
61897
61988
  installToolResultContentGuard(session);
61898
61989
  installMessageContentGuard(session, sessionManager);
61899
- session.__fusionMemoryAppendAvailable = options.customTools?.some((tool) => tool.name === "memory_append") === true;
61990
+ session.__fusionMemoryAppendAvailable = options.customTools?.some((tool) => tool.name === FN_MEMORY_APPEND_TOOL_NAME) === true;
61900
61991
  const promptableSession = session;
61901
61992
  promptableSession.promptWithFallback = async (prompt, promptOptions) => {
61902
61993
  try {
@@ -61946,8 +62037,11 @@ async function createFnAgent5(options) {
61946
62037
  const fallbackSessionResult = await createSessionWithModel(fallbackModel);
61947
62038
  const fallbackSession = fallbackSessionResult.session;
61948
62039
  installToolResultContentGuard(fallbackSession);
61949
- installMessageContentGuard(fallbackSession, sessionManager);
61950
- fallbackSession.__fusionMemoryAppendAvailable = options.customTools?.some((tool) => tool.name === "memory_append") === true;
62040
+ installMessageContentGuard(
62041
+ fallbackSession,
62042
+ sessionManager
62043
+ );
62044
+ fallbackSession.__fusionMemoryAppendAvailable = options.customTools?.some((tool) => tool.name === FN_MEMORY_APPEND_TOOL_NAME) === true;
61951
62045
  if (options.defaultThinkingLevel) {
61952
62046
  fallbackSession.setThinkingLevel(options.defaultThinkingLevel);
61953
62047
  }
@@ -62029,9 +62123,9 @@ async function createFnAgent5(options) {
62029
62123
  });
62030
62124
  return { session: promptableSession, sessionFile: promptableSession.sessionFile };
62031
62125
  }
62032
- var execAsync, COMPACTION_FALLBACK_INSTRUCTIONS, MAX_COMPACTED_PROMPT_MEMORY_CHARS;
62126
+ var execAsync, FN_MEMORY_APPEND_TOOL_NAME, COMPACTION_FALLBACK_INSTRUCTIONS, MAX_COMPACTED_PROMPT_MEMORY_CHARS;
62033
62127
  var init_pi = __esm({
62034
- "../engine/src/pi.js"() {
62128
+ "../engine/src/pi.ts"() {
62035
62129
  "use strict";
62036
62130
  init_src();
62037
62131
  init_skill_resolver();
@@ -62039,6 +62133,7 @@ var init_pi = __esm({
62039
62133
  init_auth_storage();
62040
62134
  init_logger2();
62041
62135
  execAsync = promisify2(exec);
62136
+ FN_MEMORY_APPEND_TOOL_NAME = "fn_memory_append";
62042
62137
  COMPACTION_FALLBACK_INSTRUCTIONS = [
62043
62138
  "Summarize all completed steps concisely.",
62044
62139
  "Preserve the current step number and any in-progress work details.",
@@ -75492,6 +75587,13 @@ function formatTaskIdentifier(task) {
75492
75587
  const snippet = task.description.length > maxLen ? task.description.slice(0, maxLen) + "..." : task.description;
75493
75588
  return `${task.id}: ${snippet}`;
75494
75589
  }
75590
+ function resolveNtfyBaseUrl(baseUrl, fallback2 = DEFAULT_NTFY_BASE_URL) {
75591
+ const trimmed = baseUrl?.trim();
75592
+ if (!trimmed) {
75593
+ return fallback2;
75594
+ }
75595
+ return trimmed.replace(/\/+$/, "");
75596
+ }
75495
75597
  function resolveNtfyEvents(events) {
75496
75598
  return events ? [...events] : [...DEFAULT_NTFY_EVENTS];
75497
75599
  }
@@ -75515,7 +75617,7 @@ function buildNtfyClickUrl(options) {
75515
75617
  return query ? `${normalizedHost}/?${query}` : `${normalizedHost}/`;
75516
75618
  }
75517
75619
  async function sendNtfyNotification({
75518
- ntfyBaseUrl = "https://ntfy.sh",
75620
+ ntfyBaseUrl,
75519
75621
  topic,
75520
75622
  title,
75521
75623
  message,
@@ -75532,7 +75634,8 @@ async function sendNtfyNotification({
75532
75634
  if (clickUrl) {
75533
75635
  headers.Click = clickUrl;
75534
75636
  }
75535
- const response = await fetch(`${ntfyBaseUrl}/${topic}`, {
75637
+ const resolvedBaseUrl = resolveNtfyBaseUrl(ntfyBaseUrl);
75638
+ const response = await fetch(`${resolvedBaseUrl}/${topic}`, {
75536
75639
  method: "POST",
75537
75640
  headers,
75538
75641
  body: message,
@@ -75548,11 +75651,12 @@ async function sendNtfyNotification({
75548
75651
  schedulerLog.log(`Failed to send ntfy notification: ${err}`);
75549
75652
  }
75550
75653
  }
75551
- var DEFAULT_NTFY_EVENTS, NtfyNotifier;
75654
+ var DEFAULT_NTFY_BASE_URL, DEFAULT_NTFY_EVENTS, NtfyNotifier;
75552
75655
  var init_notifier = __esm({
75553
75656
  "../engine/src/notifier.ts"() {
75554
75657
  "use strict";
75555
75658
  init_logger2();
75659
+ DEFAULT_NTFY_BASE_URL = "https://ntfy.sh";
75556
75660
  DEFAULT_NTFY_EVENTS = [
75557
75661
  "in-review",
75558
75662
  "merged",
@@ -75564,7 +75668,8 @@ var init_notifier = __esm({
75564
75668
  NtfyNotifier = class {
75565
75669
  constructor(store, options = {}) {
75566
75670
  this.store = store;
75567
- this.ntfyBaseUrl = options.ntfyBaseUrl ?? "https://ntfy.sh";
75671
+ this.defaultNtfyBaseUrl = resolveNtfyBaseUrl(options.ntfyBaseUrl);
75672
+ this.ntfyBaseUrl = this.defaultNtfyBaseUrl;
75568
75673
  this.projectId = options.projectId;
75569
75674
  }
75570
75675
  config = {
@@ -75574,6 +75679,7 @@ var init_notifier = __esm({
75574
75679
  events: [...DEFAULT_NTFY_EVENTS]
75575
75680
  };
75576
75681
  ntfyBaseUrl;
75682
+ defaultNtfyBaseUrl;
75577
75683
  projectId;
75578
75684
  notifiedEvents = /* @__PURE__ */ new Set();
75579
75685
  abortController = null;
@@ -75712,7 +75818,7 @@ var init_notifier = __esm({
75712
75818
  };
75713
75819
  handleSettingsUpdated = (data) => {
75714
75820
  const { settings, previous } = data;
75715
- if (settings.ntfyEnabled !== previous.ntfyEnabled || settings.ntfyTopic !== previous.ntfyTopic || settings.ntfyDashboardHost !== previous.ntfyDashboardHost || JSON.stringify(settings.ntfyEvents) !== JSON.stringify(previous.ntfyEvents)) {
75821
+ if (settings.ntfyEnabled !== previous.ntfyEnabled || settings.ntfyTopic !== previous.ntfyTopic || settings.ntfyBaseUrl !== previous.ntfyBaseUrl || settings.ntfyDashboardHost !== previous.ntfyDashboardHost || JSON.stringify(settings.ntfyEvents) !== JSON.stringify(previous.ntfyEvents)) {
75716
75822
  const wasEnabled = this.config.enabled;
75717
75823
  this.loadConfig(settings);
75718
75824
  if (this.config.enabled && !wasEnabled) {
@@ -75721,6 +75827,8 @@ var init_notifier = __esm({
75721
75827
  schedulerLog.log("NtfyNotifier disabled");
75722
75828
  } else if (this.config.topic !== previous.ntfyTopic) {
75723
75829
  schedulerLog.log("NtfyNotifier topic updated");
75830
+ } else if (this.ntfyBaseUrl !== resolveNtfyBaseUrl(previous.ntfyBaseUrl)) {
75831
+ schedulerLog.log("NtfyNotifier base URL updated");
75724
75832
  } else if (this.config.dashboardHost !== previous.ntfyDashboardHost) {
75725
75833
  schedulerLog.log("NtfyNotifier dashboard host updated");
75726
75834
  } else if (JSON.stringify(this.config.events) !== JSON.stringify(previous.ntfyEvents)) {
@@ -75735,6 +75843,7 @@ var init_notifier = __esm({
75735
75843
  dashboardHost: settings.ntfyDashboardHost,
75736
75844
  events: resolveNtfyEvents(settings.ntfyEvents)
75737
75845
  };
75846
+ this.ntfyBaseUrl = resolveNtfyBaseUrl(settings.ntfyBaseUrl, this.defaultNtfyBaseUrl);
75738
75847
  }
75739
75848
  isEventEnabled(event) {
75740
75849
  return isNtfyEventEnabled(this.config.events, event);
@@ -84390,6 +84499,7 @@ __export(src_exports2, {
84390
84499
  createTaskDocumentWriteTool: () => createTaskDocumentWriteTool,
84391
84500
  createTaskLogTool: () => createTaskLogTool,
84392
84501
  describeAgentModel: () => describeAgentModel,
84502
+ describeModel: () => describeModel,
84393
84503
  getDefaultPiRuntime: () => getDefaultPiRuntime,
84394
84504
  isNtfyEventEnabled: () => isNtfyEventEnabled,
84395
84505
  isUsageLimitError: () => isUsageLimitError,
@@ -112899,7 +113009,7 @@ var require_headers = __commonJS({
112899
113009
  function isGNU(buf) {
112900
113010
  return b4a.equals(GNU_MAGIC, buf.subarray(MAGIC_OFFSET, MAGIC_OFFSET + 6)) && b4a.equals(GNU_VER, buf.subarray(VERSION_OFFSET, VERSION_OFFSET + 2));
112901
113011
  }
112902
- function clamp2(index2, len, defaultValue) {
113012
+ function clamp3(index2, len, defaultValue) {
112903
113013
  if (typeof index2 !== "number") return defaultValue;
112904
113014
  index2 = ~~index2;
112905
113015
  if (index2 >= len) return len;
@@ -113018,7 +113128,7 @@ var require_headers = __commonJS({
113018
113128
  return parse256(val);
113019
113129
  } else {
113020
113130
  while (offset < val.length && val[offset] === 32) offset++;
113021
- const end = clamp2(indexOf(val, 32, offset, val.length), val.length, val.length);
113131
+ const end = clamp3(indexOf(val, 32, offset, val.length), val.length, val.length);
113022
113132
  while (offset < end && val[offset] === 0) offset++;
113023
113133
  if (end === offset) return 0;
113024
113134
  return parseInt(b4a.toString(val.subarray(offset, end)), 8);
@@ -117304,6 +117414,22 @@ function createApiRoutes(store, options) {
117304
117414
  }
117305
117415
  });
117306
117416
  router.post("/settings/test-ntfy", async (req, res) => {
117417
+ const normalizeNtfyBaseUrl = (value, source) => {
117418
+ const trimmed = value.trim();
117419
+ if (!trimmed) {
117420
+ throw badRequest("ntfy server URL cannot be empty");
117421
+ }
117422
+ let parsed;
117423
+ try {
117424
+ parsed = new URL(trimmed);
117425
+ } catch {
117426
+ throw badRequest(`ntfy server URL from ${source} must be a valid URL`);
117427
+ }
117428
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
117429
+ throw badRequest("ntfy server URL must use http:// or https://");
117430
+ }
117431
+ return trimmed.replace(/\/+$/, "");
117432
+ };
117307
117433
  try {
117308
117434
  const { store: scopedStore } = await getProjectContext2(req);
117309
117435
  const settings = await scopedStore.getSettings();
@@ -117314,7 +117440,13 @@ function createApiRoutes(store, options) {
117314
117440
  if (!topic || !/^[a-zA-Z0-9_-]{1,64}$/.test(topic)) {
117315
117441
  throw badRequest("ntfy topic is not configured or invalid");
117316
117442
  }
117317
- const ntfyBaseUrl = "https://ntfy.sh";
117443
+ const overrideValue = req.body?.ntfyBaseUrl;
117444
+ if (overrideValue !== void 0 && overrideValue !== null && typeof overrideValue !== "string") {
117445
+ throw badRequest("ntfy server URL must be a string");
117446
+ }
117447
+ const requestOverride = typeof overrideValue === "string" && overrideValue.trim() ? normalizeNtfyBaseUrl(overrideValue, "request") : void 0;
117448
+ const storedServer = typeof settings.ntfyBaseUrl === "string" && settings.ntfyBaseUrl.trim() ? normalizeNtfyBaseUrl(settings.ntfyBaseUrl, "settings") : void 0;
117449
+ const ntfyBaseUrl = requestOverride ?? storedServer ?? "https://ntfy.sh";
117318
117450
  const url = `${ntfyBaseUrl}/${topic}`;
117319
117451
  const response = await fetch(url, {
117320
117452
  method: "POST",
@@ -117326,7 +117458,7 @@ function createApiRoutes(store, options) {
117326
117458
  body: "Fusion test notification \u2014 your notifications are working!"
117327
117459
  });
117328
117460
  if (!response.ok) {
117329
- throw new ApiError(502, `ntfy.sh returned ${response.status}: ${response.statusText}`);
117461
+ throw new ApiError(502, `ntfy server returned ${response.status}: ${response.statusText}`);
117330
117462
  }
117331
117463
  res.json({ success: true });
117332
117464
  } catch (err) {
@@ -136126,50 +136258,152 @@ var init_claude_cli_extension = __esm({
136126
136258
  }
136127
136259
  });
136128
136260
 
136129
- // src/commands/dashboard-tui.ts
136130
- import * as readline from "node:readline";
136131
- function moveCursorTo(x, y) {
136132
- process.stdout.write(`\x1B[${y};${x}H`);
136133
- }
136134
- function clearLine() {
136135
- process.stdout.write("\x1B[2K");
136136
- }
136137
- function clearScreen() {
136138
- process.stdout.write("\x1B[2J");
136139
- }
136140
- function hideCursor() {
136141
- process.stdout.write("\x1B[?25l");
136142
- }
136143
- function showCursor() {
136144
- process.stdout.write("\x1B[?25h");
136145
- }
136146
- function enableAlternateScreen() {
136147
- process.stdout.write("\x1B[?47h");
136148
- }
136149
- function disableAlternateScreen() {
136150
- process.stdout.write("\x1B[?47l");
136261
+ // src/commands/dashboard-tui/log-ring-buffer.ts
136262
+ var MAX_LOG_ENTRIES, LogRingBuffer;
136263
+ var init_log_ring_buffer = __esm({
136264
+ "src/commands/dashboard-tui/log-ring-buffer.ts"() {
136265
+ "use strict";
136266
+ MAX_LOG_ENTRIES = 1e3;
136267
+ LogRingBuffer = class {
136268
+ entries = [];
136269
+ count = 0;
136270
+ push(entry) {
136271
+ if (this.entries.length < MAX_LOG_ENTRIES) {
136272
+ this.entries.push(entry);
136273
+ } else {
136274
+ this.entries[this.count % MAX_LOG_ENTRIES] = entry;
136275
+ }
136276
+ this.count++;
136277
+ }
136278
+ getAll() {
136279
+ if (this.count <= MAX_LOG_ENTRIES) {
136280
+ return this.entries.slice();
136281
+ }
136282
+ const start = this.count % MAX_LOG_ENTRIES;
136283
+ return [
136284
+ ...this.entries.slice(start),
136285
+ ...this.entries.slice(0, start)
136286
+ ];
136287
+ }
136288
+ clear() {
136289
+ this.entries = [];
136290
+ this.count = 0;
136291
+ }
136292
+ get total() {
136293
+ return this.count;
136294
+ }
136295
+ };
136296
+ }
136297
+ });
136298
+
136299
+ // src/commands/dashboard-tui/state.ts
136300
+ var SECTION_ORDER;
136301
+ var init_state = __esm({
136302
+ "src/commands/dashboard-tui/state.ts"() {
136303
+ "use strict";
136304
+ SECTION_ORDER = ["system", "logs", "utilities", "stats", "settings"];
136305
+ }
136306
+ });
136307
+
136308
+ // src/commands/dashboard-tui/logo.ts
136309
+ var FUSION_LOGO_LINES, FUSION_TAGLINE;
136310
+ var init_logo = __esm({
136311
+ "src/commands/dashboard-tui/logo.ts"() {
136312
+ "use strict";
136313
+ FUSION_LOGO_LINES = [
136314
+ "\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2557 \u2588\u2588\u2557",
136315
+ "\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551",
136316
+ "\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2554\u2588\u2588\u2557 \u2588\u2588\u2551",
136317
+ "\u2588\u2588\u2554\u2550\u2550\u255D \u2588\u2588\u2551 \u2588\u2588\u2551\u255A\u2550\u2550\u2550\u2550\u2588\u2588\u2551\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551\u255A\u2588\u2588\u2557\u2588\u2588\u2551",
136318
+ "\u2588\u2588\u2551 \u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2551\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2551 \u255A\u2588\u2588\u2588\u2588\u2551",
136319
+ "\u255A\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u2550\u2550\u255D"
136320
+ ];
136321
+ FUSION_TAGLINE = "AI coding agent dashboard";
136322
+ }
136323
+ });
136324
+
136325
+ // src/commands/dashboard-tui/hooks/use-projects.ts
136326
+ import { useState, useEffect, useCallback } from "react";
136327
+ function useProjects(interactiveData) {
136328
+ const [projects, setProjects] = useState([]);
136329
+ const [loading, setLoading] = useState(false);
136330
+ const [error, setError] = useState(null);
136331
+ useEffect(() => {
136332
+ if (!interactiveData) return;
136333
+ setLoading(true);
136334
+ setError(null);
136335
+ interactiveData.listProjects().then((p) => {
136336
+ setProjects(p);
136337
+ setLoading(false);
136338
+ }).catch((err) => {
136339
+ setProjects([]);
136340
+ setLoading(false);
136341
+ setError(err instanceof Error ? err.message : String(err));
136342
+ });
136343
+ }, [interactiveData]);
136344
+ return { projects, loading, error };
136345
+ }
136346
+ function useTasks(interactiveData, selectedProject) {
136347
+ const [tasks, setTasks] = useState([]);
136348
+ const [loading, setLoading] = useState(false);
136349
+ const [error, setError] = useState(null);
136350
+ const [reloadTick, setReloadTick] = useState(0);
136351
+ const refresh = useCallback(() => setReloadTick((n) => n + 1), []);
136352
+ useEffect(() => {
136353
+ if (!interactiveData || !selectedProject) {
136354
+ setTasks([]);
136355
+ setLoading(false);
136356
+ setError(null);
136357
+ return;
136358
+ }
136359
+ setLoading(true);
136360
+ setError(null);
136361
+ interactiveData.listTasks(selectedProject.path).then((t) => {
136362
+ setTasks(t);
136363
+ setLoading(false);
136364
+ }).catch((err) => {
136365
+ setTasks([]);
136366
+ setLoading(false);
136367
+ setError(err instanceof Error ? err.message : String(err));
136368
+ });
136369
+ }, [interactiveData, selectedProject, reloadTick]);
136370
+ return { tasks, loading, error, refresh };
136151
136371
  }
136152
- function colorize(text, color) {
136153
- const colors = {
136154
- reset: "\x1B[0m",
136155
- bold: "\x1B[1m",
136156
- dim: "\x1B[2m",
136157
- red: "\x1B[31m",
136158
- green: "\x1B[32m",
136159
- yellow: "\x1B[33m",
136160
- blue: "\x1B[34m",
136161
- magenta: "\x1B[35m",
136162
- cyan: "\x1B[36m",
136163
- white: "\x1B[37m",
136164
- gray: "\x1B[90m",
136165
- brightRed: "\x1B[91m",
136166
- brightGreen: "\x1B[92m",
136167
- brightYellow: "\x1B[93m",
136168
- brightBlue: "\x1B[94m",
136169
- brightMagenta: "\x1B[95m",
136170
- brightCyan: "\x1B[96m"
136171
- };
136172
- return `${colors[color] || ""}${text}${colors.reset}`;
136372
+ var init_use_projects = __esm({
136373
+ "src/commands/dashboard-tui/hooks/use-projects.ts"() {
136374
+ "use strict";
136375
+ }
136376
+ });
136377
+
136378
+ // src/commands/dashboard-tui/app.tsx
136379
+ var app_exports = {};
136380
+ __export(app_exports, {
136381
+ DashboardApp: () => DashboardApp
136382
+ });
136383
+ import { useState as useState2, useSyncExternalStore, useCallback as useCallback2, useEffect as useEffect2 } from "react";
136384
+ import { Box, Text, useInput, useApp, useStdout } from "ink";
136385
+ import Spinner from "ink-spinner";
136386
+ import TextInput from "ink-text-input";
136387
+ import { spawn as spawn5 } from "node:child_process";
136388
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
136389
+ function openInBrowser(url) {
136390
+ let cmd;
136391
+ let args;
136392
+ if (process.platform === "darwin") {
136393
+ cmd = "open";
136394
+ args = [url];
136395
+ } else if (process.platform === "win32") {
136396
+ cmd = "cmd";
136397
+ args = ["/c", "start", "", url];
136398
+ } else {
136399
+ cmd = "xdg-open";
136400
+ args = [url];
136401
+ }
136402
+ try {
136403
+ const child = spawn5(cmd, args, { detached: true, stdio: "ignore" });
136404
+ child.unref();
136405
+ } catch {
136406
+ }
136173
136407
  }
136174
136408
  function formatTimestamp3(date) {
136175
136409
  const h = date.getHours().toString().padStart(2, "0");
@@ -136188,170 +136422,1532 @@ function formatUptime(ms) {
136188
136422
  if (minutes > 0) return `${minutes}m ${seconds % 60}s`;
136189
136423
  return `${seconds}s`;
136190
136424
  }
136191
- function visibleLength(str) {
136192
- return str.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, "").length;
136425
+ function formatRelativeTime(iso) {
136426
+ const diff = Date.now() - new Date(iso).getTime();
136427
+ const s = Math.floor(diff / 1e3);
136428
+ if (s < 60) return `${s}s ago`;
136429
+ const m = Math.floor(s / 60);
136430
+ if (m < 60) return `${m}m ago`;
136431
+ const h = Math.floor(m / 60);
136432
+ return `${h}h ago`;
136433
+ }
136434
+ function logoColor(index2) {
136435
+ return LOGO_COLORS[Math.min(index2, LOGO_COLORS.length - 1)];
136436
+ }
136437
+ function AnimatedFusionLogo() {
136438
+ return /* @__PURE__ */ jsx(Box, { flexDirection: "column", alignItems: "center", children: FUSION_LOGO_LINES.map((line, i) => /* @__PURE__ */ jsx(Text, { color: logoColor(i), bold: true, children: line }, i)) });
136439
+ }
136440
+ function SplashScreen({ loadingStatus }) {
136441
+ const { stdout } = useStdout();
136442
+ const cols = stdout?.columns ?? 80;
136443
+ const rows = stdout?.rows ?? 24;
136444
+ const compact = cols < SPLASH_MIN_COLS || rows < SPLASH_MIN_ROWS;
136445
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", paddingX: 1, paddingY: 1, children: [
136446
+ compact ? /* @__PURE__ */ jsx(Text, { bold: true, color: "cyanBright", children: "FUSION" }) : /* @__PURE__ */ jsx(AnimatedFusionLogo, {}),
136447
+ /* @__PURE__ */ jsx(Text, { color: "blueBright", dimColor: true, children: FUSION_TAGLINE }),
136448
+ /* @__PURE__ */ jsx(Box, { height: 1 }),
136449
+ /* @__PURE__ */ jsxs(Box, { flexDirection: "row", gap: 1, children: [
136450
+ /* @__PURE__ */ jsx(Text, { color: "cyanBright", children: /* @__PURE__ */ jsx(Spinner, { type: "dots" }) }),
136451
+ /* @__PURE__ */ jsx(Text, { color: "blueBright", dimColor: true, children: loadingStatus })
136452
+ ] })
136453
+ ] });
136454
+ }
136455
+ function MiniLogo() {
136456
+ return /* @__PURE__ */ jsx(Box, { flexDirection: "row", gap: 0, children: /* @__PURE__ */ jsx(Text, { color: "cyan", bold: true, children: "FUSION" }) });
136457
+ }
136458
+ function Panel({ title, isFocused, children, flexGrow, flexShrink, width }) {
136459
+ return /* @__PURE__ */ jsxs(
136460
+ Box,
136461
+ {
136462
+ borderStyle: "round",
136463
+ borderColor: isFocused ? "cyan" : "gray",
136464
+ flexDirection: "column",
136465
+ flexGrow,
136466
+ flexShrink,
136467
+ width,
136468
+ overflow: "hidden",
136469
+ children: [
136470
+ /* @__PURE__ */ jsx(Box, { paddingX: 1, children: /* @__PURE__ */ jsx(Text, { bold: isFocused, color: isFocused ? "cyan" : void 0, dimColor: !isFocused, children: title }) }),
136471
+ /* @__PURE__ */ jsx(Box, { flexDirection: "column", paddingX: 1, flexGrow: 1, overflow: "hidden", children })
136472
+ ]
136473
+ }
136474
+ );
136193
136475
  }
136194
- function visibleTruncate(text, maxWidth) {
136195
- if (maxWidth <= 0) return "";
136196
- const currentLength = visibleLength(text);
136197
- if (currentLength <= maxWidth) return text;
136198
- const ansiRegex = /\x1b\[[0-9;]*[a-zA-Z]/g;
136199
- let result = "";
136200
- let visibleCount = 0;
136201
- let match;
136202
- const ansiMatches = [];
136203
- while ((match = ansiRegex.exec(text)) !== null) {
136204
- ansiMatches.push({
136205
- start: match.index,
136206
- end: match.index + match[0].length,
136207
- seq: match[0]
136476
+ function SystemPanel({ state, isFocused }) {
136477
+ const info = state.systemInfo;
136478
+ return /* @__PURE__ */ jsx(Panel, { title: "System", isFocused, flexGrow: 1, children: !info ? /* @__PURE__ */ jsx(Text, { dimColor: true, children: "System information not available." }) : /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
136479
+ /* @__PURE__ */ jsxs(Box, { flexDirection: "row", gap: 1, children: [
136480
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "Host:" }),
136481
+ /* @__PURE__ */ jsx(Text, { children: info.host })
136482
+ ] }),
136483
+ /* @__PURE__ */ jsxs(Box, { flexDirection: "row", gap: 1, children: [
136484
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "Port:" }),
136485
+ /* @__PURE__ */ jsx(Text, { children: info.port })
136486
+ ] }),
136487
+ /* @__PURE__ */ jsxs(Box, { flexDirection: "row", gap: 1, children: [
136488
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "URL:" }),
136489
+ /* @__PURE__ */ jsx(Text, { color: "cyan", children: info.baseUrl })
136490
+ ] }),
136491
+ info.authEnabled ? /* @__PURE__ */ jsxs(Fragment, { children: [
136492
+ /* @__PURE__ */ jsxs(Box, { flexDirection: "row", gap: 1, children: [
136493
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "Auth:" }),
136494
+ /* @__PURE__ */ jsx(Text, { color: "yellow", children: "bearer token required" })
136495
+ ] }),
136496
+ info.authToken && /* @__PURE__ */ jsxs(Box, { flexDirection: "row", gap: 1, children: [
136497
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "Token:" }),
136498
+ /* @__PURE__ */ jsx(Text, { wrap: "truncate", color: "yellow", children: info.authToken })
136499
+ ] }),
136500
+ info.tokenizedUrl && /* @__PURE__ */ jsxs(Box, { flexDirection: "row", gap: 1, children: [
136501
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "Open:" }),
136502
+ /* @__PURE__ */ jsx(Text, { wrap: "truncate", color: "cyanBright", children: info.tokenizedUrl })
136503
+ ] }),
136504
+ /* @__PURE__ */ jsxs(Box, { flexDirection: "row", gap: 1, children: [
136505
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "Press" }),
136506
+ /* @__PURE__ */ jsx(Text, { color: "cyanBright", bold: true, children: "[Enter]" }),
136507
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "to open in browser" })
136508
+ ] })
136509
+ ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
136510
+ /* @__PURE__ */ jsxs(Box, { flexDirection: "row", gap: 1, children: [
136511
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "Auth:" }),
136512
+ /* @__PURE__ */ jsx(Text, { color: "yellow", children: "no auth" })
136513
+ ] }),
136514
+ /* @__PURE__ */ jsxs(Box, { flexDirection: "row", gap: 1, children: [
136515
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "Press" }),
136516
+ /* @__PURE__ */ jsx(Text, { color: "cyanBright", bold: true, children: "[Enter]" }),
136517
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "to open in browser" })
136518
+ ] })
136519
+ ] }),
136520
+ /* @__PURE__ */ jsxs(Box, { flexDirection: "row", gap: 1, children: [
136521
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "Engine:" }),
136522
+ info.engineMode === "dev" && /* @__PURE__ */ jsx(Text, { color: "yellow", children: "dev" }),
136523
+ info.engineMode === "paused" && /* @__PURE__ */ jsx(Text, { color: "yellow", children: "paused" }),
136524
+ info.engineMode === "active" && /* @__PURE__ */ jsx(Text, { color: "green", children: "active" })
136525
+ ] }),
136526
+ /* @__PURE__ */ jsxs(Box, { flexDirection: "row", gap: 1, children: [
136527
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "Watcher:" }),
136528
+ info.fileWatcher ? /* @__PURE__ */ jsx(Text, { color: "green", children: "active" }) : /* @__PURE__ */ jsx(Text, { color: "red", children: "inactive" })
136529
+ ] }),
136530
+ /* @__PURE__ */ jsxs(Box, { flexDirection: "row", gap: 1, children: [
136531
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "Uptime:" }),
136532
+ /* @__PURE__ */ jsx(Text, { children: formatUptime(Date.now() - info.startTimeMs) })
136533
+ ] })
136534
+ ] }) });
136535
+ }
136536
+ function StatsPanel({ state, isFocused }) {
136537
+ const stats = state.taskStats;
136538
+ return /* @__PURE__ */ jsx(Panel, { title: "Stats", isFocused, flexGrow: 1, children: !stats ? /* @__PURE__ */ jsx(Text, { dimColor: true, children: "Statistics not available." }) : /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
136539
+ /* @__PURE__ */ jsxs(Box, { flexDirection: "row", gap: 1, children: [
136540
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "Total:" }),
136541
+ /* @__PURE__ */ jsx(Text, { children: stats.total })
136542
+ ] }),
136543
+ Object.entries(stats.byColumn).map(([col, count]) => {
136544
+ const name = col.replace(/-/g, " ");
136545
+ const isActive = (col === "in-progress" || col === "in-review") && count > 0;
136546
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "row", gap: 1, marginLeft: 1, children: [
136547
+ /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
136548
+ name,
136549
+ ":"
136550
+ ] }),
136551
+ /* @__PURE__ */ jsx(Text, { color: isActive ? "green" : void 0, children: count })
136552
+ ] }, col);
136553
+ }),
136554
+ /* @__PURE__ */ jsx(Box, { height: 1 }),
136555
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "Agents:" }),
136556
+ /* @__PURE__ */ jsxs(Box, { marginLeft: 1, flexDirection: "column", children: [
136557
+ /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
136558
+ "idle: ",
136559
+ /* @__PURE__ */ jsx(Text, { color: "white", children: stats.agents.idle })
136560
+ ] }),
136561
+ /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
136562
+ "active: ",
136563
+ /* @__PURE__ */ jsx(Text, { color: "green", children: stats.agents.active })
136564
+ ] }),
136565
+ /* @__PURE__ */ jsxs(Text, { color: stats.agents.error > 0 ? "red" : void 0, dimColor: stats.agents.error === 0, children: [
136566
+ "error: ",
136567
+ stats.agents.error
136568
+ ] })
136569
+ ] })
136570
+ ] }) });
136571
+ }
136572
+ function SettingsPanel({ state, isFocused }) {
136573
+ const s = state.settings;
136574
+ return /* @__PURE__ */ jsx(Panel, { title: "Settings", isFocused, flexGrow: 1, children: !s ? /* @__PURE__ */ jsx(Text, { dimColor: true, children: "Settings not available." }) : /* @__PURE__ */ jsx(Box, { flexDirection: "column", children: [
136575
+ ["maxConcurrent", s.maxConcurrent.toString()],
136576
+ ["maxWorktrees", s.maxWorktrees.toString()],
136577
+ ["autoMerge", s.autoMerge ? "enabled" : "disabled"],
136578
+ ["mergeStrategy", s.mergeStrategy],
136579
+ ["pollMs", `${s.pollIntervalMs}`],
136580
+ ["paused", s.enginePaused ? "yes" : "no"],
136581
+ ["globalPause", s.globalPause ? "yes" : "no"]
136582
+ ].map(([key, value]) => {
136583
+ const isEnabled = value === "enabled" || value === "yes";
136584
+ const isDisabled = value === "disabled" || value === "no";
136585
+ const color = isEnabled ? "green" : isDisabled ? "yellow" : void 0;
136586
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "row", gap: 1, children: [
136587
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: key }),
136588
+ /* @__PURE__ */ jsx(Text, { color, children: value })
136589
+ ] }, key);
136590
+ }) }) });
136591
+ }
136592
+ function LevelBadge({ level }) {
136593
+ if (level === "error") return /* @__PURE__ */ jsx(Text, { color: "red", children: "\u2717" });
136594
+ if (level === "warn") return /* @__PURE__ */ jsx(Text, { color: "yellow", children: "\u26A0" });
136595
+ return /* @__PURE__ */ jsx(Text, { color: "green", children: "\u2713" });
136596
+ }
136597
+ function LogsPanel({
136598
+ state,
136599
+ isFocused,
136600
+ availableRows
136601
+ }) {
136602
+ const { logsSeverityFilter, logsWrapEnabled, logsExpandedMode, selectedLogIndex } = state;
136603
+ const entries = logsSeverityFilter === "all" ? state.logEntries : state.logEntries.filter((e) => e.level === logsSeverityFilter);
136604
+ const cursor = entries.length === 0 ? 0 : Math.min(Math.max(selectedLogIndex, 0), entries.length - 1);
136605
+ const rowBudget = Math.max(1, availableRows);
136606
+ const visibleStart = Math.max(0, Math.min(
136607
+ cursor - Math.floor(rowBudget / 2),
136608
+ entries.length - rowBudget
136609
+ ));
136610
+ const visibleEnd = Math.min(entries.length, visibleStart + rowBudget);
136611
+ const visibleEntries = entries.slice(visibleStart, visibleEnd);
136612
+ const hiddenAbove = visibleStart;
136613
+ const hiddenBelow = entries.length - visibleEnd;
136614
+ return /* @__PURE__ */ jsx(Panel, { title: `Logs (${state.logEntries.length}/1000)`, isFocused, flexGrow: 1, children: logsExpandedMode && entries[cursor] ? /* @__PURE__ */ jsx(
136615
+ ExpandedLog,
136616
+ {
136617
+ entry: entries[cursor],
136618
+ index: cursor,
136619
+ total: entries.length
136620
+ }
136621
+ ) : entries.length === 0 ? /* @__PURE__ */ jsx(Text, { dimColor: true, children: "No log entries yet." }) : entries.length !== state.logEntries.length && entries.length === 0 ? /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
136622
+ "No entries match filter ",
136623
+ logsSeverityFilter.toUpperCase(),
136624
+ "."
136625
+ ] }) : /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
136626
+ /* @__PURE__ */ jsxs(Box, { flexDirection: "row", gap: 1, marginBottom: 0, children: [
136627
+ /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
136628
+ "[w] wrap ",
136629
+ logsWrapEnabled ? "on" : "off"
136630
+ ] }),
136631
+ /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
136632
+ "[f] ",
136633
+ logsSeverityFilter
136634
+ ] }),
136635
+ hiddenAbove > 0 && /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
136636
+ "\u2191 ",
136637
+ hiddenAbove,
136638
+ " more"
136639
+ ] }),
136640
+ hiddenBelow > 0 && /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
136641
+ "\u2193 ",
136642
+ hiddenBelow,
136643
+ " more"
136644
+ ] })
136645
+ ] }),
136646
+ visibleEntries.map((entry, displayIdx) => {
136647
+ const absoluteIndex = visibleStart + displayIdx;
136648
+ const isSelected = absoluteIndex === cursor;
136649
+ const bg = isSelected ? "blue" : void 0;
136650
+ const fg = isSelected ? "whiteBright" : void 0;
136651
+ const ts = formatTimestamp3(entry.timestamp);
136652
+ const lvl = entry.level === "error" ? "\u2717" : entry.level === "warn" ? "\u26A0" : "\u2713";
136653
+ const lvlColor = entry.level === "error" ? "red" : entry.level === "warn" ? "yellow" : "green";
136654
+ const prefixSlot = entry.prefix ? `[${entry.prefix}]`.slice(0, PREFIX_WIDTH).padEnd(PREFIX_WIDTH) : " ".repeat(PREFIX_WIDTH);
136655
+ const marker = isSelected ? "\u25B6 " : " ";
136656
+ return /* @__PURE__ */ jsxs(
136657
+ Text,
136658
+ {
136659
+ backgroundColor: bg,
136660
+ wrap: logsWrapEnabled ? "wrap" : "truncate-end",
136661
+ children: [
136662
+ /* @__PURE__ */ jsx(Text, { color: isSelected ? "cyanBright" : "gray", bold: isSelected, children: marker }),
136663
+ /* @__PURE__ */ jsxs(Text, { color: fg, dimColor: !isSelected, children: [
136664
+ ts,
136665
+ " "
136666
+ ] }),
136667
+ /* @__PURE__ */ jsx(Text, { color: lvlColor, children: lvl }),
136668
+ /* @__PURE__ */ jsx(Text, { color: fg, dimColor: !isSelected, children: ` ${prefixSlot} ` }),
136669
+ /* @__PURE__ */ jsx(Text, { color: fg, bold: isSelected, children: entry.message })
136670
+ ]
136671
+ },
136672
+ `${entry.timestamp.getTime()}-${displayIdx}`
136673
+ );
136674
+ })
136675
+ ] }) });
136676
+ }
136677
+ function ExpandedLog({ entry, index: index2, total }) {
136678
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
136679
+ /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
136680
+ "Entry ",
136681
+ index2 + 1,
136682
+ "/",
136683
+ total,
136684
+ " \xB7 [Enter/Esc] close"
136685
+ ] }),
136686
+ /* @__PURE__ */ jsx(Box, { height: 1 }),
136687
+ /* @__PURE__ */ jsxs(Box, { flexDirection: "row", gap: 1, children: [
136688
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "Time:" }),
136689
+ /* @__PURE__ */ jsx(Text, { children: formatTimestamp3(entry.timestamp) })
136690
+ ] }),
136691
+ /* @__PURE__ */ jsxs(Box, { flexDirection: "row", gap: 1, children: [
136692
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "Level:" }),
136693
+ /* @__PURE__ */ jsx(LevelBadge, { level: entry.level }),
136694
+ /* @__PURE__ */ jsx(Text, { children: entry.level.toUpperCase() })
136695
+ ] }),
136696
+ entry.prefix && /* @__PURE__ */ jsxs(Box, { flexDirection: "row", gap: 1, children: [
136697
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "Prefix:" }),
136698
+ /* @__PURE__ */ jsx(Text, { children: entry.prefix })
136699
+ ] }),
136700
+ /* @__PURE__ */ jsx(Box, { height: 1 }),
136701
+ /* @__PURE__ */ jsx(Text, { wrap: "wrap", children: entry.message })
136702
+ ] });
136703
+ }
136704
+ function UtilitiesPanel({ isFocused }) {
136705
+ const actions = [
136706
+ { key: "r", label: "Refresh Stats" },
136707
+ { key: "c", label: "Clear Logs" },
136708
+ { key: "t", label: "Toggle Engine Pause" },
136709
+ { key: "?", label: "Help" }
136710
+ ];
136711
+ return /* @__PURE__ */ jsx(Panel, { title: "Utilities", isFocused, flexShrink: 0, children: /* @__PURE__ */ jsx(Box, { flexDirection: "column", children: actions.map((action) => /* @__PURE__ */ jsxs(Box, { flexDirection: "row", gap: 1, children: [
136712
+ /* @__PURE__ */ jsxs(Text, { color: "yellow", children: [
136713
+ "[",
136714
+ action.key,
136715
+ "]"
136716
+ ] }),
136717
+ /* @__PURE__ */ jsx(Text, { children: action.label })
136718
+ ] }, action.key)) }) });
136719
+ }
136720
+ function HelpOverlay() {
136721
+ const shortcuts = [
136722
+ ["[b]", "Board view (interactive mode)"],
136723
+ ["[a]", "Agents view"],
136724
+ ["[g]", "Settings view"],
136725
+ ["[s]", "Status mode"],
136726
+ ["[1] / [2] / [3]", "Board / Agents / Settings (interactive)"],
136727
+ ["[Tab]", "Cycle focused panel forward"],
136728
+ ["[Shift+Tab]", "Cycle focused panel backward"],
136729
+ ["[1-5]", "Jump to panel by number (status mode)"],
136730
+ ["[\u2192] / [n]", "Next panel (status mode)"],
136731
+ ["[\u2190] / [p]", "Previous panel (status mode)"],
136732
+ ["[r]", "Refresh stats (Utilities)"],
136733
+ ["[c]", "Clear logs (Utilities)"],
136734
+ ["[t]", "Toggle engine pause (Utilities)"],
136735
+ ["[\u2191/\u2193/k/j]", "Navigate log entries (Logs)"],
136736
+ ["[Home/End]", "First/last log entry (Logs)"],
136737
+ ["[Enter/Space/e]", "Expand log entry (Logs)"],
136738
+ ["[w]", "Toggle word wrap (Logs)"],
136739
+ ["[f]", "Cycle severity filter (Logs)"],
136740
+ ["[s/x]", "Start/stop agent (Agents view)"],
136741
+ ["[D]", "Delete agent \u2014 requires confirm (Agents view)"],
136742
+ ["[r]", "Refresh agent detail (Agents view)"],
136743
+ ["[Space]", "Toggle boolean (Settings view)"],
136744
+ ["[+/-]", "Adjust number (Settings view)"],
136745
+ ["[?] / [h]", "Toggle help"],
136746
+ ["[q]", "Quit"],
136747
+ ["[Ctrl+C]", "Force quit"]
136748
+ ];
136749
+ const rowKeyWidth = 22;
136750
+ const rowDescWidth = Math.max(...shortcuts.map(([, d]) => d.length));
136751
+ const innerWidth = rowKeyWidth + 2 + rowDescWidth + 2;
136752
+ const titleRow = " KEYBOARD SHORTCUTS".padEnd(innerWidth);
136753
+ return /* @__PURE__ */ jsxs(Box, { borderStyle: "round", borderColor: "cyan", flexDirection: "column", backgroundColor: "black", children: [
136754
+ /* @__PURE__ */ jsx(Text, { backgroundColor: "black", bold: true, color: "cyanBright", children: titleRow }),
136755
+ /* @__PURE__ */ jsx(Text, { backgroundColor: "black", children: " " }),
136756
+ shortcuts.map(([key, desc]) => {
136757
+ const keyCell = ` ${key.padEnd(rowKeyWidth - 1)} `;
136758
+ const descCell = ` ${desc.padEnd(rowDescWidth)} `;
136759
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "row", children: [
136760
+ /* @__PURE__ */ jsx(Text, { backgroundColor: "black", color: "yellow", children: keyCell }),
136761
+ /* @__PURE__ */ jsx(Text, { backgroundColor: "black", color: "white", children: descCell })
136762
+ ] }, key);
136763
+ })
136764
+ ] });
136765
+ }
136766
+ function StatusModeGrid({
136767
+ state,
136768
+ rows,
136769
+ controller
136770
+ }) {
136771
+ const focused = state.activeSection;
136772
+ const bodyRows = Math.max(8, rows - 7);
136773
+ const logsAvailableRows = Math.max(4, bodyRows - 4);
136774
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [
136775
+ /* @__PURE__ */ jsxs(Box, { flexDirection: "row", gap: 1, paddingX: 1, paddingY: 0, children: [
136776
+ /* @__PURE__ */ jsx(MiniLogo, {}),
136777
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2502" }),
136778
+ SECTION_ORDER.map((section, i) => {
136779
+ const isActive = section === focused;
136780
+ const label = section.charAt(0).toUpperCase() + section.slice(1);
136781
+ return /* @__PURE__ */ jsx(Box, { marginRight: 1, children: isActive ? /* @__PURE__ */ jsx(Text, { backgroundColor: "cyan", color: "black", bold: true, children: ` [${i + 1}] ${label} ` }) : /* @__PURE__ */ jsx(Text, { dimColor: true, children: `[${i + 1}] ${label}` }) }, section);
136782
+ }),
136783
+ /* @__PURE__ */ jsx(Box, { flexGrow: 1 }),
136784
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "[b] board [a] agents [g] settings [?] help [q] quit" })
136785
+ ] }),
136786
+ /* @__PURE__ */ jsxs(Box, { flexDirection: "row", flexGrow: 1, overflow: "hidden", children: [
136787
+ /* @__PURE__ */ jsxs(Box, { flexDirection: "column", flexGrow: 1, overflow: "hidden", children: [
136788
+ /* @__PURE__ */ jsx(SystemPanel, { state, isFocused: focused === "system" }),
136789
+ /* @__PURE__ */ jsx(StatsPanel, { state, isFocused: focused === "stats" }),
136790
+ /* @__PURE__ */ jsx(SettingsPanel, { state, isFocused: focused === "settings" })
136791
+ ] }),
136792
+ /* @__PURE__ */ jsxs(Box, { flexDirection: "column", flexGrow: 2, overflow: "hidden", children: [
136793
+ /* @__PURE__ */ jsx(
136794
+ LogsPanel,
136795
+ {
136796
+ state,
136797
+ isFocused: focused === "logs",
136798
+ availableRows: logsAvailableRows
136799
+ }
136800
+ ),
136801
+ /* @__PURE__ */ jsx(UtilitiesPanel, { isFocused: focused === "utilities" })
136802
+ ] })
136803
+ ] }),
136804
+ /* @__PURE__ */ jsx(StatusBar, { state, controller })
136805
+ ] });
136806
+ }
136807
+ function StatusModeSingle({
136808
+ state,
136809
+ controller
136810
+ }) {
136811
+ const focused = state.activeSection;
136812
+ const activePanel = () => {
136813
+ switch (focused) {
136814
+ case "system":
136815
+ return /* @__PURE__ */ jsx(SystemPanel, { state, isFocused: true });
136816
+ case "logs":
136817
+ return /* @__PURE__ */ jsx(LogsPanel, { state, isFocused: true, availableRows: Math.max(4, (process.stdout.rows ?? 24) - 8) });
136818
+ case "utilities":
136819
+ return /* @__PURE__ */ jsx(UtilitiesPanel, { isFocused: true });
136820
+ case "stats":
136821
+ return /* @__PURE__ */ jsx(StatsPanel, { state, isFocused: true });
136822
+ case "settings":
136823
+ return /* @__PURE__ */ jsx(SettingsPanel, { state, isFocused: true });
136824
+ }
136825
+ };
136826
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [
136827
+ /* @__PURE__ */ jsxs(Box, { flexDirection: "row", gap: 1, paddingX: 1, children: [
136828
+ /* @__PURE__ */ jsx(MiniLogo, {}),
136829
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2502" }),
136830
+ SECTION_ORDER.map((section, i) => {
136831
+ const isActive = section === focused;
136832
+ const label = section.charAt(0).toUpperCase() + section.slice(1);
136833
+ return /* @__PURE__ */ jsx(Box, { marginRight: 1, children: isActive ? /* @__PURE__ */ jsx(Text, { backgroundColor: "cyan", color: "black", bold: true, children: ` [${i + 1}] ${label} ` }) : /* @__PURE__ */ jsx(Text, { dimColor: true, children: `[${i + 1}] ${label}` }) }, section);
136834
+ })
136835
+ ] }),
136836
+ /* @__PURE__ */ jsx(Box, { flexGrow: 1, flexDirection: "column", overflow: "hidden", children: activePanel() }),
136837
+ /* @__PURE__ */ jsx(StatusBar, { state, controller })
136838
+ ] });
136839
+ }
136840
+ function StatusBar({ state, controller: _controller }) {
136841
+ const { systemInfo, activeSection } = state;
136842
+ const hotkeys = [];
136843
+ if (activeSection === "logs") {
136844
+ hotkeys.push("\u2191\u2193 navigate", "w wrap", "f filter", "Enter expand");
136845
+ } else if (activeSection === "utilities") {
136846
+ hotkeys.push("r refresh", "c clear logs", "t toggle pause");
136847
+ } else {
136848
+ hotkeys.push("Tab cycle panel", "1-5 jump");
136849
+ }
136850
+ const statusParts = [];
136851
+ if (systemInfo) {
136852
+ statusParts.push(systemInfo.baseUrl);
136853
+ statusParts.push(formatUptime(Date.now() - systemInfo.startTimeMs));
136854
+ }
136855
+ return /* @__PURE__ */ jsxs(Box, { justifyContent: "space-between", paddingX: 1, children: [
136856
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: hotkeys.join(" \xB7 ") }),
136857
+ statusParts.length > 0 && /* @__PURE__ */ jsx(Text, { dimColor: true, children: statusParts.join(" | ") })
136858
+ ] });
136859
+ }
136860
+ function InteractiveHeader({ activeView }) {
136861
+ const tabs = [
136862
+ { key: "b", label: "Board", view: "board" },
136863
+ { key: "a", label: "Agents", view: "agents" },
136864
+ { key: "g", label: "Settings", view: "settings" }
136865
+ ];
136866
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "row", gap: 2, paddingX: 1, children: [
136867
+ /* @__PURE__ */ jsx(MiniLogo, {}),
136868
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2502" }),
136869
+ tabs.map(({ key, label, view }) => {
136870
+ const isActive = view === activeView;
136871
+ return isActive ? /* @__PURE__ */ jsx(Text, { backgroundColor: "cyan", color: "black", bold: true, children: ` [${key}] ${label} ` }, view) : /* @__PURE__ */ jsx(Text, { dimColor: true, children: `[${key}] ${label}` }, view);
136872
+ }),
136873
+ /* @__PURE__ */ jsx(Box, { flexGrow: 1 }),
136874
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "[s] status [?] help [q] quit" })
136875
+ ] });
136876
+ }
136877
+ function columnLabel(col) {
136878
+ return col.replace(/-/g, " ").replace(/\b\w/g, (l) => l.toUpperCase());
136879
+ }
136880
+ function TaskCard({
136881
+ task,
136882
+ selected,
136883
+ width
136884
+ }) {
136885
+ const accent = COLUMN_COLORS[task.column] ?? "white";
136886
+ const borderColor = selected ? "cyanBright" : "gray";
136887
+ const titleColor = selected ? "whiteBright" : void 0;
136888
+ const shortId = task.id.length > 10 ? task.id.slice(0, 8) : task.id;
136889
+ return /* @__PURE__ */ jsxs(
136890
+ Box,
136891
+ {
136892
+ borderStyle: "round",
136893
+ borderColor,
136894
+ flexDirection: "column",
136895
+ paddingX: 1,
136896
+ width,
136897
+ flexShrink: 0,
136898
+ children: [
136899
+ /* @__PURE__ */ jsxs(Box, { flexDirection: "row", justifyContent: "space-between", children: [
136900
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: shortId }),
136901
+ task.agentState && /* @__PURE__ */ jsxs(Text, { color: accent, children: [
136902
+ "\u25CF ",
136903
+ task.agentState
136904
+ ] })
136905
+ ] }),
136906
+ /* @__PURE__ */ jsx(Text, { bold: selected, color: titleColor, wrap: "truncate-end", children: task.title ?? task.id })
136907
+ ]
136908
+ }
136909
+ );
136910
+ }
136911
+ function KanbanColumnView({
136912
+ column,
136913
+ tasks,
136914
+ isFocused,
136915
+ selectedIndex,
136916
+ width
136917
+ }) {
136918
+ const accent = COLUMN_COLORS[column];
136919
+ const headerColor = isFocused ? "whiteBright" : accent;
136920
+ const cardWidth = Math.max(12, width - 4);
136921
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", width, flexShrink: 0, paddingX: 1, children: [
136922
+ /* @__PURE__ */ jsxs(Box, { flexDirection: "row", gap: 1, children: [
136923
+ /* @__PURE__ */ jsx(Text, { bold: true, color: headerColor, backgroundColor: isFocused ? accent : void 0, children: ` ${columnLabel(column).toUpperCase()} ` }),
136924
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: tasks.length })
136925
+ ] }),
136926
+ /* @__PURE__ */ jsx(Box, { height: 1 }),
136927
+ tasks.length === 0 ? /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2014" }) : /* @__PURE__ */ jsx(Box, { flexDirection: "column", gap: 0, children: tasks.map((task, i) => /* @__PURE__ */ jsx(
136928
+ TaskCard,
136929
+ {
136930
+ task,
136931
+ selected: isFocused && i === selectedIndex,
136932
+ width: cardWidth
136933
+ },
136934
+ task.id
136935
+ )) })
136936
+ ] });
136937
+ }
136938
+ function ProjectSelector({
136939
+ open,
136940
+ projects,
136941
+ selectedIndex,
136942
+ onSelect: _onSelect
136943
+ }) {
136944
+ const current = projects[selectedIndex] ?? null;
136945
+ if (!open) {
136946
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "row", gap: 1, children: [
136947
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "Project:" }),
136948
+ /* @__PURE__ */ jsx(Text, { bold: true, color: "cyanBright", children: current?.name ?? "(none)" }),
136949
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "[p] change" })
136950
+ ] });
136951
+ }
136952
+ return /* @__PURE__ */ jsxs(
136953
+ Box,
136954
+ {
136955
+ borderStyle: "round",
136956
+ borderColor: "cyan",
136957
+ flexDirection: "column",
136958
+ paddingX: 1,
136959
+ backgroundColor: "black",
136960
+ width: Math.max(30, ...projects.map((p) => p.name.length + 4)),
136961
+ children: [
136962
+ /* @__PURE__ */ jsx(Text, { bold: true, color: "cyan", backgroundColor: "black", children: "Pick a project" }),
136963
+ projects.length === 0 ? /* @__PURE__ */ jsx(Text, { dimColor: true, backgroundColor: "black", children: "(no projects registered)" }) : projects.map((p, i) => {
136964
+ const isSel = i === selectedIndex;
136965
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "row", gap: 1, backgroundColor: "black", children: [
136966
+ /* @__PURE__ */ jsx(Text, { color: isSel ? "cyanBright" : "gray", backgroundColor: "black", children: isSel ? "\u25B6" : " " }),
136967
+ /* @__PURE__ */ jsx(Text, { bold: isSel, color: isSel ? "whiteBright" : void 0, backgroundColor: "black", children: p.name })
136968
+ ] }, p.id);
136969
+ }),
136970
+ /* @__PURE__ */ jsx(Box, { height: 1 }),
136971
+ /* @__PURE__ */ jsx(Text, { dimColor: true, backgroundColor: "black", children: "\u2191\u2193 navigate \xB7 Enter select \xB7 Esc cancel" })
136972
+ ]
136973
+ }
136974
+ );
136975
+ }
136976
+ function TaskDetailScreen({ task }) {
136977
+ const accent = COLUMN_COLORS[task.column] ?? "white";
136978
+ return /* @__PURE__ */ jsxs(
136979
+ Box,
136980
+ {
136981
+ borderStyle: "round",
136982
+ borderColor: "cyan",
136983
+ flexDirection: "column",
136984
+ paddingX: 2,
136985
+ paddingY: 1,
136986
+ flexGrow: 1,
136987
+ children: [
136988
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: task.id }),
136989
+ /* @__PURE__ */ jsx(Box, { height: 1 }),
136990
+ /* @__PURE__ */ jsx(Text, { bold: true, color: "whiteBright", wrap: "wrap", children: task.title ?? task.id }),
136991
+ /* @__PURE__ */ jsx(Box, { height: 1 }),
136992
+ /* @__PURE__ */ jsxs(Box, { flexDirection: "row", gap: 2, children: [
136993
+ /* @__PURE__ */ jsxs(Box, { children: [
136994
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "Column " }),
136995
+ /* @__PURE__ */ jsx(Text, { color: accent, bold: true, children: columnLabel(task.column) })
136996
+ ] }),
136997
+ task.agentState && /* @__PURE__ */ jsxs(Box, { children: [
136998
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "Agent " }),
136999
+ /* @__PURE__ */ jsx(Text, { color: accent, bold: true, children: task.agentState })
137000
+ ] })
137001
+ ] }),
137002
+ task.description && /* @__PURE__ */ jsxs(Fragment, { children: [
137003
+ /* @__PURE__ */ jsx(Box, { height: 1 }),
137004
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2500\u2500\u2500\u2500 Description \u2500\u2500\u2500\u2500" }),
137005
+ /* @__PURE__ */ jsx(Box, { height: 1 }),
137006
+ /* @__PURE__ */ jsx(Text, { wrap: "wrap", children: task.description })
137007
+ ] }),
137008
+ /* @__PURE__ */ jsx(Box, { flexGrow: 1 }),
137009
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "[Esc / Backspace] back to board \xB7 [q] quit" })
137010
+ ]
137011
+ }
137012
+ );
137013
+ }
137014
+ function clamp2(n, min, max) {
137015
+ return Math.max(min, Math.min(max, n));
137016
+ }
137017
+ function groupTasksByColumn(tasks) {
137018
+ const out = {
137019
+ todo: [],
137020
+ "in-progress": [],
137021
+ "in-review": [],
137022
+ done: []
137023
+ };
137024
+ for (const task of tasks) {
137025
+ const col = KANBAN_COLUMNS.includes(task.column) ? task.column : "todo";
137026
+ out[col].push(task);
137027
+ }
137028
+ return out;
137029
+ }
137030
+ function BoardView({ state }) {
137031
+ const { stdout } = useStdout();
137032
+ const cols = stdout?.columns ?? 80;
137033
+ const columnWidth = Math.max(20, Math.floor((cols - 2) / KANBAN_COLUMNS.length));
137034
+ const [subView, setSubView] = useState2("board");
137035
+ const [projectIndex, setProjectIndex] = useState2(0);
137036
+ const [colIndex, setColIndex] = useState2(0);
137037
+ const [rowByColumn, setRowByColumn] = useState2({
137038
+ todo: 0,
137039
+ "in-progress": 0,
137040
+ "in-review": 0,
137041
+ done: 0
137042
+ });
137043
+ const [pickerOriginal, setPickerOriginal] = useState2(0);
137044
+ const [newTaskTitle, setNewTaskTitle] = useState2("");
137045
+ const [createError, setCreateError] = useState2(null);
137046
+ const [creating, setCreating] = useState2(false);
137047
+ const projectsState = useProjects(state.interactiveData);
137048
+ const selectedProject = projectsState.projects[projectIndex] ?? null;
137049
+ const tasksState = useTasks(state.interactiveData, selectedProject);
137050
+ const grouped = groupTasksByColumn(tasksState.tasks);
137051
+ const focusedColumn = KANBAN_COLUMNS[colIndex];
137052
+ const focusedTasks = grouped[focusedColumn];
137053
+ const focusedRow = clamp2(rowByColumn[focusedColumn] ?? 0, 0, Math.max(0, focusedTasks.length - 1));
137054
+ const selectedTask = focusedTasks[focusedRow] ?? null;
137055
+ useInput((input, key) => {
137056
+ if (subView === "picker") {
137057
+ if (key.upArrow || input === "k") {
137058
+ setProjectIndex((p) => Math.max(0, p - 1));
137059
+ return;
137060
+ }
137061
+ if (key.downArrow || input === "j") {
137062
+ setProjectIndex((p) => Math.min(projectsState.projects.length - 1, p + 1));
137063
+ return;
137064
+ }
137065
+ if (key.return) {
137066
+ setSubView("board");
137067
+ return;
137068
+ }
137069
+ if (key.escape) {
137070
+ setProjectIndex(pickerOriginal);
137071
+ setSubView("board");
137072
+ return;
137073
+ }
137074
+ return;
137075
+ }
137076
+ if (subView === "detail") {
137077
+ if (key.escape || key.backspace || input === "h") {
137078
+ setSubView("board");
137079
+ }
137080
+ return;
137081
+ }
137082
+ if (subView === "create") {
137083
+ if (key.escape) {
137084
+ setSubView("board");
137085
+ setNewTaskTitle("");
137086
+ setCreateError(null);
137087
+ }
137088
+ return;
137089
+ }
137090
+ if (input === "p" || input === "P") {
137091
+ setPickerOriginal(projectIndex);
137092
+ setSubView("picker");
137093
+ return;
137094
+ }
137095
+ if (input === "n" || input === "N") {
137096
+ if (!selectedProject) return;
137097
+ setNewTaskTitle("");
137098
+ setCreateError(null);
137099
+ setSubView("create");
137100
+ return;
137101
+ }
137102
+ if (key.return) {
137103
+ if (selectedTask) setSubView("detail");
137104
+ return;
137105
+ }
137106
+ if (key.leftArrow || input === "h") {
137107
+ setColIndex((c) => Math.max(0, c - 1));
137108
+ return;
137109
+ }
137110
+ if (key.rightArrow || input === "l") {
137111
+ setColIndex((c) => Math.min(KANBAN_COLUMNS.length - 1, c + 1));
137112
+ return;
137113
+ }
137114
+ if (key.upArrow || input === "k") {
137115
+ setRowByColumn((m) => ({
137116
+ ...m,
137117
+ [focusedColumn]: Math.max(0, (m[focusedColumn] ?? 0) - 1)
137118
+ }));
137119
+ return;
137120
+ }
137121
+ if (key.downArrow || input === "j") {
137122
+ setRowByColumn((m) => {
137123
+ const len = grouped[focusedColumn].length;
137124
+ return { ...m, [focusedColumn]: Math.min(Math.max(0, len - 1), (m[focusedColumn] ?? 0) + 1) };
137125
+ });
137126
+ return;
137127
+ }
137128
+ });
137129
+ const hintText = subView === "picker" ? "\u2191\u2193 pick \xB7 Enter confirm \xB7 Esc cancel" : subView === "detail" ? "Esc back \xB7 q quit" : subView === "create" ? "type a task title \xB7 Enter create \xB7 Esc cancel" : "\u2190\u2192 column \xB7 \u2191\u2193 task \xB7 Enter open \xB7 n new \xB7 p project";
137130
+ const submitNewTask = async () => {
137131
+ const title = newTaskTitle.trim();
137132
+ if (!title) {
137133
+ setCreateError("Title cannot be empty");
137134
+ return;
137135
+ }
137136
+ if (!state.interactiveData || !selectedProject) {
137137
+ setCreateError("No project selected");
137138
+ return;
137139
+ }
137140
+ setCreating(true);
137141
+ setCreateError(null);
137142
+ try {
137143
+ await state.interactiveData.createTask(selectedProject.path, { title });
137144
+ setNewTaskTitle("");
137145
+ setSubView("board");
137146
+ tasksState.refresh();
137147
+ } catch (err) {
137148
+ setCreateError(err instanceof Error ? err.message : String(err));
137149
+ } finally {
137150
+ setCreating(false);
137151
+ }
137152
+ };
137153
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [
137154
+ /* @__PURE__ */ jsxs(Box, { flexDirection: "row", gap: 2, paddingX: 1, children: [
137155
+ /* @__PURE__ */ jsx(
137156
+ ProjectSelector,
137157
+ {
137158
+ open: subView === "picker",
137159
+ projects: projectsState.projects,
137160
+ selectedIndex: projectIndex,
137161
+ onSelect: setProjectIndex
137162
+ }
137163
+ ),
137164
+ /* @__PURE__ */ jsx(Box, { flexGrow: 1 }),
137165
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: hintText })
137166
+ ] }),
137167
+ /* @__PURE__ */ jsx(Box, { height: 1 }),
137168
+ subView === "create" ? /* @__PURE__ */ jsx(Box, { justifyContent: "center", alignItems: "center", flexGrow: 1, children: /* @__PURE__ */ jsxs(
137169
+ Box,
137170
+ {
137171
+ borderStyle: "round",
137172
+ borderColor: "cyan",
137173
+ flexDirection: "column",
137174
+ paddingX: 2,
137175
+ paddingY: 1,
137176
+ width: Math.min(80, Math.max(40, cols - 8)),
137177
+ children: [
137178
+ /* @__PURE__ */ jsx(Text, { bold: true, color: "cyanBright", children: "New Task" }),
137179
+ /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
137180
+ "Project: ",
137181
+ selectedProject?.name ?? "(none)"
137182
+ ] }),
137183
+ /* @__PURE__ */ jsx(Box, { height: 1 }),
137184
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "Title" }),
137185
+ /* @__PURE__ */ jsxs(Box, { children: [
137186
+ /* @__PURE__ */ jsx(Text, { color: "cyanBright", children: "\u25B8 " }),
137187
+ /* @__PURE__ */ jsx(
137188
+ TextInput,
137189
+ {
137190
+ value: newTaskTitle,
137191
+ onChange: setNewTaskTitle,
137192
+ onSubmit: () => void submitNewTask(),
137193
+ placeholder: "What needs doing?"
137194
+ }
137195
+ )
137196
+ ] }),
137197
+ /* @__PURE__ */ jsx(Box, { height: 1 }),
137198
+ createError && /* @__PURE__ */ jsx(Text, { color: "red", children: createError }),
137199
+ creating ? /* @__PURE__ */ jsxs(Box, { flexDirection: "row", gap: 1, children: [
137200
+ /* @__PURE__ */ jsx(Text, { color: "cyanBright", children: /* @__PURE__ */ jsx(Spinner, { type: "dots" }) }),
137201
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "Creating\u2026" })
137202
+ ] }) : /* @__PURE__ */ jsx(Text, { dimColor: true, children: "Enter to create \xB7 Esc to cancel" })
137203
+ ]
137204
+ }
137205
+ ) }) : subView === "detail" && selectedTask ? /* @__PURE__ */ jsx(Box, { flexGrow: 1, paddingX: 1, children: /* @__PURE__ */ jsx(TaskDetailScreen, { task: selectedTask }) }) : tasksState.loading ? /* @__PURE__ */ jsxs(Box, { justifyContent: "center", alignItems: "center", flexGrow: 1, gap: 1, children: [
137206
+ /* @__PURE__ */ jsx(Text, { color: "cyanBright", children: /* @__PURE__ */ jsx(Spinner, { type: "dots" }) }),
137207
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "Loading tasks\u2026" })
137208
+ ] }) : tasksState.tasks.length === 0 ? /* @__PURE__ */ jsxs(Box, { justifyContent: "center", alignItems: "center", flexGrow: 1, flexDirection: "column", children: [
137209
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "No tasks in this project." }),
137210
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "Press [p] to switch projects." })
137211
+ ] }) : /* @__PURE__ */ jsx(Box, { flexDirection: "row", flexGrow: 1, overflow: "hidden", children: KANBAN_COLUMNS.map((col, i) => /* @__PURE__ */ jsx(
137212
+ KanbanColumnView,
137213
+ {
137214
+ column: col,
137215
+ tasks: grouped[col],
137216
+ isFocused: i === colIndex,
137217
+ selectedIndex: rowByColumn[col] ?? 0,
137218
+ width: columnWidth
137219
+ },
137220
+ col
137221
+ )) })
137222
+ ] });
137223
+ }
137224
+ function agentStateColor(state) {
137225
+ switch (state) {
137226
+ case "active":
137227
+ return "cyan";
137228
+ case "running":
137229
+ return "green";
137230
+ case "error":
137231
+ return "red";
137232
+ default:
137233
+ return "gray";
137234
+ }
137235
+ }
137236
+ function heartbeatFreshness(lastHeartbeatAt) {
137237
+ if (!lastHeartbeatAt) return { fresh: false, label: "never" };
137238
+ const ageMs = Date.now() - new Date(lastHeartbeatAt).getTime();
137239
+ return { fresh: ageMs < 5 * 60 * 1e3, label: formatRelativeTime(lastHeartbeatAt) };
137240
+ }
137241
+ function AgentsView({ state }) {
137242
+ const { stdout } = useStdout();
137243
+ const cols = stdout?.columns ?? 80;
137244
+ const isNarrow = cols < 80;
137245
+ const [selectedIndex, setSelectedIndex] = useState2(0);
137246
+ const [agents, setAgents] = useState2([]);
137247
+ const [detail, setDetail] = useState2(null);
137248
+ const [loadingDetail, setLoadingDetail] = useState2(false);
137249
+ const [subView, setSubView] = useState2("list");
137250
+ const [statusMsg, setStatusMsg] = useState2(null);
137251
+ const [detailFocused, setDetailFocused] = useState2(false);
137252
+ const data = state.interactiveData;
137253
+ useEffect2(() => {
137254
+ if (!data) return;
137255
+ data.listAgents().then(setAgents).catch(() => setAgents([]));
137256
+ }, [data]);
137257
+ const selectedAgent = agents[selectedIndex] ?? null;
137258
+ useEffect2(() => {
137259
+ if (!data || !selectedAgent) {
137260
+ setDetail(null);
137261
+ return;
137262
+ }
137263
+ setLoadingDetail(true);
137264
+ data.getAgentDetail(selectedAgent.id).then((d) => {
137265
+ setDetail(d);
137266
+ setLoadingDetail(false);
137267
+ }).catch(() => {
137268
+ setDetail(null);
137269
+ setLoadingDetail(false);
137270
+ });
137271
+ }, [data, selectedAgent?.id]);
137272
+ function refreshDetail() {
137273
+ if (!data || !selectedAgent) return;
137274
+ setLoadingDetail(true);
137275
+ data.getAgentDetail(selectedAgent.id).then((d) => {
137276
+ setDetail(d);
137277
+ setLoadingDetail(false);
137278
+ }).catch(() => {
137279
+ setDetail(null);
137280
+ setLoadingDetail(false);
136208
137281
  });
136209
137282
  }
136210
- for (let i = 0; i < text.length; i++) {
136211
- const char = text[i];
136212
- const ansiMatch = ansiMatches.find((m) => m.start === i);
136213
- if (ansiMatch) {
136214
- result += ansiMatch.seq;
136215
- continue;
137283
+ async function refreshList() {
137284
+ if (!data) return;
137285
+ const list = await data.listAgents().catch(() => []);
137286
+ setAgents(list);
137287
+ setSelectedIndex((i) => Math.min(i, Math.max(0, list.length - 1)));
137288
+ }
137289
+ useInput((input, key) => {
137290
+ if (subView === "confirm-delete") {
137291
+ if (input === "y" || input === "Y") {
137292
+ if (!data || !selectedAgent) {
137293
+ setSubView("list");
137294
+ return;
137295
+ }
137296
+ data.deleteAgent(selectedAgent.id).then(() => {
137297
+ setStatusMsg(`Deleted agent ${selectedAgent.name}`);
137298
+ return refreshList();
137299
+ }).catch((err) => setStatusMsg(`Error: ${err instanceof Error ? err.message : String(err)}`)).finally(() => setSubView("list"));
137300
+ return;
137301
+ }
137302
+ setSubView("list");
137303
+ return;
136216
137304
  }
136217
- visibleCount++;
136218
- result += char;
136219
- if (visibleCount >= maxWidth - 3) {
136220
- break;
137305
+ if (key.tab) {
137306
+ setDetailFocused((f) => !f);
137307
+ return;
137308
+ }
137309
+ if (!detailFocused) {
137310
+ if (key.upArrow || input === "k") {
137311
+ setSelectedIndex((i) => Math.max(0, i - 1));
137312
+ return;
137313
+ }
137314
+ if (key.downArrow || input === "j") {
137315
+ setSelectedIndex((i) => Math.min(agents.length - 1, i + 1));
137316
+ return;
137317
+ }
137318
+ }
137319
+ if (input === "s") {
137320
+ if (!data || !selectedAgent) return;
137321
+ data.updateAgentState(selectedAgent.id, "active").then(() => {
137322
+ setStatusMsg("Agent started");
137323
+ return refreshList();
137324
+ }).catch((err) => setStatusMsg(`Error: ${err instanceof Error ? err.message : String(err)}`));
137325
+ return;
137326
+ }
137327
+ if (input === "x") {
137328
+ if (!data || !selectedAgent) return;
137329
+ data.updateAgentState(selectedAgent.id, "idle").then(() => {
137330
+ setStatusMsg("Agent stopped");
137331
+ return refreshList();
137332
+ }).catch((err) => setStatusMsg(`Error: ${err instanceof Error ? err.message : String(err)}`));
137333
+ return;
137334
+ }
137335
+ if (input === "D") {
137336
+ if (selectedAgent) setSubView("confirm-delete");
137337
+ return;
137338
+ }
137339
+ if (input === "r") {
137340
+ refreshDetail();
137341
+ return;
137342
+ }
137343
+ });
137344
+ if (subView === "confirm-delete" && selectedAgent) {
137345
+ return /* @__PURE__ */ jsx(Box, { flexDirection: "column", flexGrow: 1, justifyContent: "center", alignItems: "center", children: /* @__PURE__ */ jsxs(Box, { borderStyle: "round", borderColor: "red", flexDirection: "column", paddingX: 2, paddingY: 1, children: [
137346
+ /* @__PURE__ */ jsx(Text, { bold: true, color: "red", children: "Delete agent?" }),
137347
+ /* @__PURE__ */ jsx(Box, { height: 1 }),
137348
+ /* @__PURE__ */ jsxs(Text, { children: [
137349
+ "Agent: ",
137350
+ /* @__PURE__ */ jsx(Text, { bold: true, color: "whiteBright", children: selectedAgent.name })
137351
+ ] }),
137352
+ /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
137353
+ "ID: ",
137354
+ selectedAgent.id
137355
+ ] }),
137356
+ /* @__PURE__ */ jsx(Box, { height: 1 }),
137357
+ /* @__PURE__ */ jsx(Text, { children: "[y] confirm delete [any other key] cancel" })
137358
+ ] }) });
137359
+ }
137360
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [
137361
+ statusMsg && /* @__PURE__ */ jsx(Box, { paddingX: 1, children: /* @__PURE__ */ jsx(Text, { color: "yellow", children: statusMsg }) }),
137362
+ /* @__PURE__ */ jsxs(Box, { flexDirection: isNarrow ? "column" : "row", flexGrow: 1, overflow: "hidden", children: [
137363
+ /* @__PURE__ */ jsxs(
137364
+ Box,
137365
+ {
137366
+ borderStyle: "round",
137367
+ borderColor: detailFocused ? "gray" : "cyan",
137368
+ flexDirection: "column",
137369
+ width: isNarrow ? void 0 : "30%",
137370
+ flexGrow: isNarrow ? 1 : 0,
137371
+ flexShrink: 0,
137372
+ overflow: "hidden",
137373
+ children: [
137374
+ /* @__PURE__ */ jsx(Box, { paddingX: 1, children: /* @__PURE__ */ jsxs(Text, { bold: !detailFocused, color: !detailFocused ? "cyan" : void 0, dimColor: detailFocused, children: [
137375
+ "Agents (",
137376
+ agents.length,
137377
+ ")"
137378
+ ] }) }),
137379
+ /* @__PURE__ */ jsx(Box, { flexDirection: "column", paddingX: 1, flexGrow: 1, overflow: "hidden", children: agents.length === 0 ? /* @__PURE__ */ jsx(Text, { dimColor: true, children: "No agents found." }) : agents.map((agent, i) => {
137380
+ const isSel = i === selectedIndex;
137381
+ const { fresh, label } = heartbeatFreshness(agent.lastHeartbeatAt);
137382
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "row", gap: 1, children: [
137383
+ /* @__PURE__ */ jsx(Text, { color: isSel ? "cyanBright" : "gray", children: isSel ? "\u25B6" : " " }),
137384
+ /* @__PURE__ */ jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [
137385
+ /* @__PURE__ */ jsxs(Box, { flexDirection: "row", gap: 1, children: [
137386
+ /* @__PURE__ */ jsx(Text, { bold: isSel, color: isSel ? "whiteBright" : void 0, wrap: "truncate", children: agent.name }),
137387
+ /* @__PURE__ */ jsx(Text, { color: agentStateColor(agent.state), children: agent.state })
137388
+ ] }),
137389
+ /* @__PURE__ */ jsxs(Box, { flexDirection: "row", gap: 1, children: [
137390
+ /* @__PURE__ */ jsx(Text, { color: fresh ? "green" : "gray", dimColor: true, children: "\u25CF" }),
137391
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: label })
137392
+ ] })
137393
+ ] })
137394
+ ] }, agent.id);
137395
+ }) })
137396
+ ]
137397
+ }
137398
+ ),
137399
+ /* @__PURE__ */ jsxs(
137400
+ Box,
137401
+ {
137402
+ borderStyle: "round",
137403
+ borderColor: detailFocused ? "cyan" : "gray",
137404
+ flexDirection: "column",
137405
+ flexGrow: 1,
137406
+ overflow: "hidden",
137407
+ children: [
137408
+ /* @__PURE__ */ jsx(Box, { paddingX: 1, children: /* @__PURE__ */ jsx(Text, { bold: detailFocused, color: detailFocused ? "cyan" : void 0, dimColor: !detailFocused, children: "Agent Detail" }) }),
137409
+ /* @__PURE__ */ jsx(Box, { flexDirection: "column", paddingX: 1, flexGrow: 1, overflow: "hidden", children: !selectedAgent ? /* @__PURE__ */ jsx(Text, { dimColor: true, children: "Select an agent from the list." }) : loadingDetail ? /* @__PURE__ */ jsxs(Box, { flexDirection: "row", gap: 1, children: [
137410
+ /* @__PURE__ */ jsx(Text, { color: "cyanBright", children: /* @__PURE__ */ jsx(Spinner, { type: "dots" }) }),
137411
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "Loading\u2026" })
137412
+ ] }) : !detail ? /* @__PURE__ */ jsx(Text, { dimColor: true, children: "Could not load agent detail." }) : /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
137413
+ /* @__PURE__ */ jsx(Text, { bold: true, color: "whiteBright", children: detail.name }),
137414
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: detail.id }),
137415
+ /* @__PURE__ */ jsx(Box, { height: 1 }),
137416
+ /* @__PURE__ */ jsxs(Box, { flexDirection: "row", gap: 1, children: [
137417
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "State:" }),
137418
+ /* @__PURE__ */ jsx(Text, { color: agentStateColor(detail.state), bold: true, children: detail.state })
137419
+ ] }),
137420
+ /* @__PURE__ */ jsxs(Box, { flexDirection: "row", gap: 1, children: [
137421
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "Role:" }),
137422
+ /* @__PURE__ */ jsx(Text, { children: detail.role })
137423
+ ] }),
137424
+ detail.title && /* @__PURE__ */ jsxs(Box, { flexDirection: "row", gap: 1, children: [
137425
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "Title:" }),
137426
+ /* @__PURE__ */ jsx(Text, { children: detail.title })
137427
+ ] }),
137428
+ detail.taskId && /* @__PURE__ */ jsxs(Box, { flexDirection: "row", gap: 1, children: [
137429
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "Task:" }),
137430
+ /* @__PURE__ */ jsx(Text, { color: "cyan", children: detail.taskId })
137431
+ ] }),
137432
+ detail.capabilities.length > 0 && /* @__PURE__ */ jsxs(Box, { flexDirection: "row", gap: 1, children: [
137433
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "Caps:" }),
137434
+ /* @__PURE__ */ jsx(Text, { children: detail.capabilities.join(", ") })
137435
+ ] }),
137436
+ detail.recentRuns.length > 0 && /* @__PURE__ */ jsxs(Fragment, { children: [
137437
+ /* @__PURE__ */ jsx(Box, { height: 1 }),
137438
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "Recent runs (latest first):" }),
137439
+ detail.recentRuns.slice(0, 5).map((run) => /* @__PURE__ */ jsxs(Box, { flexDirection: "row", gap: 1, marginLeft: 1, children: [
137440
+ /* @__PURE__ */ jsx(Text, { color: run.status === "completed" ? "green" : run.status === "failed" ? "red" : "yellow", children: run.status.slice(0, 4) }),
137441
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: run.startedAt.slice(11, 19) }),
137442
+ run.triggerDetail && /* @__PURE__ */ jsx(Text, { dimColor: true, children: run.triggerDetail })
137443
+ ] }, run.id))
137444
+ ] })
137445
+ ] }) })
137446
+ ]
137447
+ }
137448
+ )
137449
+ ] }),
137450
+ /* @__PURE__ */ jsx(Box, { paddingX: 1, children: /* @__PURE__ */ jsx(Text, { dimColor: true, children: "[s] start [x] stop [D] delete [r] refresh [Tab] focus \u2191\u2193 select" }) })
137451
+ ] });
137452
+ }
137453
+ function SettingsInteractiveView({ state }) {
137454
+ const [selectedIndex, setSelectedIndex] = useState2(0);
137455
+ const [localSettings, setLocalSettings] = useState2(null);
137456
+ const [models, setModels] = useState2([]);
137457
+ const [saving, setSaving] = useState2(false);
137458
+ const [statusMsg, setStatusMsg] = useState2(null);
137459
+ const [detailFocused, setDetailFocused] = useState2(false);
137460
+ const data = state.interactiveData;
137461
+ useEffect2(() => {
137462
+ if (!data) return;
137463
+ data.getSettings().then(setLocalSettings).catch(() => {
137464
+ });
137465
+ setModels(data.listModels());
137466
+ }, [data]);
137467
+ const selectedDef = SETTING_DEFS[selectedIndex];
137468
+ async function saveField(partial) {
137469
+ if (!data || !localSettings) return;
137470
+ setSaving(true);
137471
+ try {
137472
+ await data.updateSettings(partial);
137473
+ const updated = await data.getSettings();
137474
+ setLocalSettings(updated);
137475
+ setStatusMsg("Saved");
137476
+ } catch (err) {
137477
+ setStatusMsg(`Error: ${err instanceof Error ? err.message : String(err)}`);
137478
+ } finally {
137479
+ setSaving(false);
136221
137480
  }
136222
137481
  }
136223
- if (visibleCount >= maxWidth - 3 && visibleCount < currentLength) {
136224
- result += "...";
136225
- }
136226
- return result;
136227
- }
136228
- function formatConsoleArgs(args, fallbackLevel = "info") {
136229
- const stringified = args.map((arg) => {
136230
- if (typeof arg === "string") return arg;
136231
- if (arg instanceof Error) return arg.stack ?? arg.message;
136232
- if (arg === null || arg === void 0) return String(arg);
136233
- if (typeof arg === "object") {
136234
- try {
136235
- return JSON.stringify(arg);
136236
- } catch {
136237
- return String(arg);
137482
+ useInput((input, key) => {
137483
+ if (key.tab) {
137484
+ setDetailFocused((f) => !f);
137485
+ return;
137486
+ }
137487
+ if (!detailFocused) {
137488
+ if (key.upArrow || input === "k") {
137489
+ setSelectedIndex((i) => Math.max(0, i - 1));
137490
+ return;
137491
+ }
137492
+ if (key.downArrow || input === "j") {
137493
+ setSelectedIndex((i) => Math.min(SETTING_DEFS.length - 1, i + 1));
137494
+ return;
136238
137495
  }
137496
+ return;
136239
137497
  }
136240
- return String(arg);
136241
- }).join(" ");
136242
- const markerMatch = stringified.match(LOG_LEVEL_MARKER_REGEX);
136243
- const level = markerMatch?.[1];
136244
- const withoutMarker = markerMatch ? stringified.replace(LOG_LEVEL_MARKER_REGEX, "") : stringified;
136245
- const match = withoutMarker.match(/^\[([^\]]+)\]\s*(.*)$/s);
136246
- if (match) {
136247
- return { prefix: match[1], message: match[2], level: level ?? fallbackLevel };
137498
+ if (!selectedDef || !localSettings) return;
137499
+ if (selectedDef.type === "boolean" && input === " ") {
137500
+ const current = localSettings[selectedDef.key];
137501
+ const updated = { ...localSettings, [selectedDef.key]: !current };
137502
+ setLocalSettings(updated);
137503
+ void saveField({ [selectedDef.key]: !current });
137504
+ return;
137505
+ }
137506
+ if (selectedDef.type === "number") {
137507
+ const current = localSettings[selectedDef.key];
137508
+ if (input === "+" || input === "=") {
137509
+ const step = selectedDef.key === "pollIntervalMs" ? 5e3 : 1;
137510
+ const updated = { ...localSettings, [selectedDef.key]: current + step };
137511
+ setLocalSettings(updated);
137512
+ void saveField({ [selectedDef.key]: current + step });
137513
+ return;
137514
+ }
137515
+ if (input === "-" || input === "_") {
137516
+ const step = selectedDef.key === "pollIntervalMs" ? 5e3 : 1;
137517
+ const newVal = Math.max(0, current - step);
137518
+ const updated = { ...localSettings, [selectedDef.key]: newVal };
137519
+ setLocalSettings(updated);
137520
+ void saveField({ [selectedDef.key]: newVal });
137521
+ return;
137522
+ }
137523
+ }
137524
+ if (selectedDef.type === "enum" && selectedDef.options) {
137525
+ const current = localSettings[selectedDef.key];
137526
+ const idx = selectedDef.options.indexOf(current);
137527
+ if (key.rightArrow || input === "l") {
137528
+ const next = selectedDef.options[(idx + 1) % selectedDef.options.length];
137529
+ const updated = { ...localSettings, [selectedDef.key]: next };
137530
+ setLocalSettings(updated);
137531
+ void saveField({ [selectedDef.key]: next });
137532
+ return;
137533
+ }
137534
+ if (key.leftArrow || input === "h") {
137535
+ const prev = selectedDef.options[(idx - 1 + selectedDef.options.length) % selectedDef.options.length];
137536
+ const updated = { ...localSettings, [selectedDef.key]: prev };
137537
+ setLocalSettings(updated);
137538
+ void saveField({ [selectedDef.key]: prev });
137539
+ return;
137540
+ }
137541
+ }
137542
+ });
137543
+ function renderValue(def, settings) {
137544
+ const v = settings[def.key];
137545
+ if (def.type === "boolean") {
137546
+ return /* @__PURE__ */ jsx(Text, { color: v ? "green" : "yellow", children: v ? "enabled" : "disabled" });
137547
+ }
137548
+ if (def.type === "enum") {
137549
+ return /* @__PURE__ */ jsx(Text, { color: "cyan", children: String(v) });
137550
+ }
137551
+ return /* @__PURE__ */ jsx(Text, { children: String(v) });
136248
137552
  }
136249
- return { message: withoutMarker, level: level ?? fallbackLevel };
136250
- }
136251
- function centerText(text, width, padChar = " ") {
136252
- const visibleLen = visibleLength(text);
136253
- const padding = Math.max(0, width - visibleLen);
136254
- const leftPad = Math.floor(padding / 2);
136255
- const rightPad = padding - leftPad;
136256
- return padChar.repeat(leftPad) + text + padChar.repeat(rightPad);
136257
- }
136258
- function isTTYAvailable() {
136259
- return Boolean(process.stdout.isTTY && process.stdin.isTTY);
136260
- }
136261
- var MAX_LOG_ENTRIES, LogRingBuffer, SECTION_ORDER, DashboardTUI, DashboardLogSink, LOG_LEVEL_MARKER_REGEX;
136262
- var init_dashboard_tui = __esm({
136263
- "src/commands/dashboard-tui.ts"() {
136264
- "use strict";
136265
- MAX_LOG_ENTRIES = 1e3;
136266
- LogRingBuffer = class {
136267
- entries = [];
136268
- count = 0;
136269
- push(entry) {
136270
- if (this.entries.length < MAX_LOG_ENTRIES) {
136271
- this.entries.push(entry);
136272
- } else {
136273
- this.entries[this.count % MAX_LOG_ENTRIES] = entry;
137553
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [
137554
+ statusMsg && /* @__PURE__ */ jsx(Box, { paddingX: 1, children: /* @__PURE__ */ jsx(Text, { color: "yellow", children: saving ? "Saving\u2026" : statusMsg }) }),
137555
+ /* @__PURE__ */ jsxs(Box, { flexDirection: "row", flexGrow: 1, overflow: "hidden", children: [
137556
+ /* @__PURE__ */ jsxs(
137557
+ Box,
137558
+ {
137559
+ borderStyle: "round",
137560
+ borderColor: detailFocused ? "gray" : "cyan",
137561
+ flexDirection: "column",
137562
+ width: "35%",
137563
+ overflow: "hidden",
137564
+ children: [
137565
+ /* @__PURE__ */ jsx(Box, { paddingX: 1, children: /* @__PURE__ */ jsx(Text, { bold: !detailFocused, color: !detailFocused ? "cyan" : void 0, dimColor: detailFocused, children: "Settings" }) }),
137566
+ /* @__PURE__ */ jsx(Box, { flexDirection: "column", paddingX: 1, flexGrow: 1, overflow: "hidden", children: !localSettings ? /* @__PURE__ */ jsx(Text, { dimColor: true, children: "Loading\u2026" }) : SETTING_DEFS.map((def, i) => {
137567
+ const isSel = i === selectedIndex;
137568
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "row", gap: 1, children: [
137569
+ /* @__PURE__ */ jsx(Text, { color: isSel ? "cyanBright" : "gray", children: isSel ? "\u25B6" : " " }),
137570
+ /* @__PURE__ */ jsx(Text, { bold: isSel, color: isSel ? "whiteBright" : void 0, wrap: "truncate", children: def.label }),
137571
+ /* @__PURE__ */ jsx(Box, { flexGrow: 1 }),
137572
+ renderValue(def, localSettings)
137573
+ ] }, def.key);
137574
+ }) })
137575
+ ]
136274
137576
  }
136275
- this.count++;
137577
+ ),
137578
+ /* @__PURE__ */ jsxs(
137579
+ Box,
137580
+ {
137581
+ borderStyle: "round",
137582
+ borderColor: detailFocused ? "cyan" : "gray",
137583
+ flexDirection: "column",
137584
+ flexGrow: 1,
137585
+ overflow: "hidden",
137586
+ children: [
137587
+ /* @__PURE__ */ jsx(Box, { paddingX: 1, children: /* @__PURE__ */ jsx(Text, { bold: detailFocused, color: detailFocused ? "cyan" : void 0, dimColor: !detailFocused, children: "Edit / Models" }) }),
137588
+ /* @__PURE__ */ jsx(Box, { flexDirection: "column", paddingX: 1, flexGrow: 1, overflow: "hidden", children: !localSettings ? /* @__PURE__ */ jsx(Text, { dimColor: true, children: "Loading settings\u2026" }) : !selectedDef ? null : /* @__PURE__ */ jsxs(Fragment, { children: [
137589
+ /* @__PURE__ */ jsx(Text, { bold: true, color: "whiteBright", children: selectedDef.label }),
137590
+ /* @__PURE__ */ jsx(Box, { height: 1 }),
137591
+ /* @__PURE__ */ jsxs(Box, { flexDirection: "row", gap: 1, children: [
137592
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "Current:" }),
137593
+ renderValue(selectedDef, localSettings)
137594
+ ] }),
137595
+ /* @__PURE__ */ jsx(Box, { height: 1 }),
137596
+ selectedDef.type === "boolean" && /* @__PURE__ */ jsx(Text, { dimColor: true, children: "[Space] toggle" }),
137597
+ selectedDef.type === "number" && /* @__PURE__ */ jsx(Text, { dimColor: true, children: selectedDef.key === "pollIntervalMs" ? "[+/-] adjust by 5000ms" : "[+/-] adjust by 1" }),
137598
+ selectedDef.type === "enum" && selectedDef.options && /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
137599
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "[\u2190/\u2192] cycle options:" }),
137600
+ selectedDef.options.map((opt) => /* @__PURE__ */ jsxs(Box, { flexDirection: "row", gap: 1, marginLeft: 1, children: [
137601
+ /* @__PURE__ */ jsx(Text, { color: localSettings[selectedDef.key] === opt ? "cyanBright" : "gray", children: localSettings[selectedDef.key] === opt ? "\u25B6" : " " }),
137602
+ /* @__PURE__ */ jsx(Text, { children: opt })
137603
+ ] }, opt))
137604
+ ] }),
137605
+ models.length > 0 && /* @__PURE__ */ jsxs(Fragment, { children: [
137606
+ /* @__PURE__ */ jsx(Box, { height: 1 }),
137607
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2500\u2500\u2500\u2500 Available Models \u2500\u2500\u2500\u2500" }),
137608
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "Configure default model in web dashboard" }),
137609
+ /* @__PURE__ */ jsx(Box, { height: 1 }),
137610
+ models.slice(0, 8).map((m) => /* @__PURE__ */ jsxs(Box, { flexDirection: "row", gap: 1, children: [
137611
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: m.provider }),
137612
+ /* @__PURE__ */ jsx(Text, { wrap: "truncate", children: m.name }),
137613
+ /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
137614
+ Math.round(m.contextWindow / 1e3),
137615
+ "k ctx"
137616
+ ] })
137617
+ ] }, `${m.provider}/${m.id}`)),
137618
+ models.length > 8 && /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
137619
+ "\u2026 and ",
137620
+ models.length - 8,
137621
+ " more"
137622
+ ] })
137623
+ ] })
137624
+ ] }) })
137625
+ ]
137626
+ }
137627
+ )
137628
+ ] }),
137629
+ /* @__PURE__ */ jsx(Box, { paddingX: 1, children: /* @__PURE__ */ jsx(Text, { dimColor: true, children: "[Tab] switch panel \u2191\u2193 select setting [Space] toggle bool [+/-] adjust num [\u2190/\u2192] cycle enum" }) })
137630
+ ] });
137631
+ }
137632
+ function InteractiveMode({ state }) {
137633
+ if (state.interactiveData === null) {
137634
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [
137635
+ /* @__PURE__ */ jsx(InteractiveHeader, { activeView: state.interactiveView }),
137636
+ /* @__PURE__ */ jsx(Box, { justifyContent: "center", alignItems: "center", flexGrow: 1, children: /* @__PURE__ */ jsx(Text, { dimColor: true, children: "Interactive mode unavailable \u2014 no data source" }) })
137637
+ ] });
137638
+ }
137639
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [
137640
+ /* @__PURE__ */ jsx(InteractiveHeader, { activeView: state.interactiveView }),
137641
+ /* @__PURE__ */ jsx(Box, { height: 1 }),
137642
+ /* @__PURE__ */ jsxs(Box, { flexGrow: 1, overflow: "hidden", children: [
137643
+ state.interactiveView === "board" && /* @__PURE__ */ jsx(BoardView, { state }),
137644
+ state.interactiveView === "agents" && /* @__PURE__ */ jsx(AgentsView, { state }),
137645
+ state.interactiveView === "settings" && /* @__PURE__ */ jsx(SettingsInteractiveView, { state })
137646
+ ] })
137647
+ ] });
137648
+ }
137649
+ function DashboardApp({ controller }) {
137650
+ const { exit } = useApp();
137651
+ const { stdout } = useStdout();
137652
+ const cols = stdout?.columns ?? 80;
137653
+ const rows = stdout?.rows ?? 24;
137654
+ const state = useSyncExternalStore(
137655
+ useCallback2((cb) => controller.subscribe(cb), [controller]),
137656
+ useCallback2(() => controller.getSnapshot(), [controller])
137657
+ );
137658
+ useInput((input, key) => {
137659
+ if (input === "q" || input === "Q" || key.ctrl && input === "c") {
137660
+ void controller.stop();
137661
+ exit();
137662
+ process.exit(0);
137663
+ }
137664
+ if (input === "b" || input === "B") {
137665
+ controller.setMode("interactive");
137666
+ controller.setInteractiveView("board");
137667
+ return;
137668
+ }
137669
+ if (input === "a" || input === "A") {
137670
+ controller.setMode("interactive");
137671
+ controller.setInteractiveView("agents");
137672
+ return;
137673
+ }
137674
+ if (input === "g" || input === "G") {
137675
+ controller.setMode("interactive");
137676
+ controller.setInteractiveView("settings");
137677
+ return;
137678
+ }
137679
+ if (input === "s" || input === "S") {
137680
+ if (state.mode === "interactive") {
137681
+ controller.setMode("status");
137682
+ return;
136276
137683
  }
136277
- getAll() {
136278
- if (this.count <= MAX_LOG_ENTRIES) {
136279
- return this.entries.slice();
137684
+ }
137685
+ if (state.mode === "interactive") {
137686
+ if (input === "1") {
137687
+ controller.setInteractiveView("board");
137688
+ return;
137689
+ }
137690
+ if (input === "2") {
137691
+ controller.setInteractiveView("agents");
137692
+ return;
137693
+ }
137694
+ if (input === "3") {
137695
+ controller.setInteractiveView("settings");
137696
+ return;
137697
+ }
137698
+ return;
137699
+ }
137700
+ if (input === "?" || input === "h" || input === "H") {
137701
+ controller.setShowHelp(!state.showHelp);
137702
+ return;
137703
+ }
137704
+ if (key.return && state.activeSection === "system" && state.systemInfo) {
137705
+ const url = state.systemInfo.tokenizedUrl ?? state.systemInfo.baseUrl;
137706
+ openInBrowser(url);
137707
+ return;
137708
+ }
137709
+ if (input >= "1" && input <= "5") {
137710
+ const section = SECTION_ORDER[parseInt(input, 10) - 1];
137711
+ if (section) {
137712
+ controller.setActiveSection(section);
137713
+ }
137714
+ return;
137715
+ }
137716
+ if (key.tab) {
137717
+ const shift = key.shift;
137718
+ const idx = PANEL_ORDER.indexOf(state.activeSection);
137719
+ if (shift) {
137720
+ controller.setActiveSection(PANEL_ORDER[(idx - 1 + PANEL_ORDER.length) % PANEL_ORDER.length]);
137721
+ } else {
137722
+ controller.setActiveSection(PANEL_ORDER[(idx + 1) % PANEL_ORDER.length]);
137723
+ }
137724
+ return;
137725
+ }
137726
+ if (key.rightArrow || input === "n" || input === "N") {
137727
+ if (state.activeSection !== "logs" || !state.logsExpandedMode) {
137728
+ controller.cycleSection(1);
137729
+ }
137730
+ return;
137731
+ }
137732
+ if (key.leftArrow || input === "p" || input === "P") {
137733
+ if (state.activeSection !== "logs" || !state.logsExpandedMode) {
137734
+ controller.cycleSection(-1);
137735
+ }
137736
+ return;
137737
+ }
137738
+ if (state.activeSection === "utilities") {
137739
+ void controller.handleUtilityAction(input);
137740
+ return;
137741
+ }
137742
+ if (state.activeSection === "logs") {
137743
+ const filteredEntries = controller.getFilteredLogEntries();
137744
+ if (key.escape) {
137745
+ if (state.logsExpandedMode) {
137746
+ controller.setLogsExpandedMode(false);
137747
+ controller.setShowHelp(false);
137748
+ } else if (state.showHelp) {
137749
+ controller.setShowHelp(false);
136280
137750
  }
136281
- const start = this.count % MAX_LOG_ENTRIES;
136282
- return [
136283
- ...this.entries.slice(start),
136284
- ...this.entries.slice(0, start)
136285
- ];
137751
+ return;
136286
137752
  }
136287
- clear() {
136288
- this.entries = [];
136289
- this.count = 0;
137753
+ if (key.return || input === " " || input === "e" || input === "E") {
137754
+ if (filteredEntries.length > 0) {
137755
+ controller.setLogsExpandedMode(!state.logsExpandedMode);
137756
+ }
137757
+ return;
136290
137758
  }
136291
- get total() {
136292
- return this.count;
137759
+ if (input === "w" || input === "W") {
137760
+ controller.setLogsWrapEnabled(!state.logsWrapEnabled);
137761
+ return;
136293
137762
  }
136294
- };
136295
- SECTION_ORDER = ["system", "logs", "utilities", "stats", "settings"];
137763
+ if (input === "f" || input === "F") {
137764
+ controller.cycleSeverityFilter();
137765
+ return;
137766
+ }
137767
+ if (key.upArrow || input === "k" || input === "K") {
137768
+ if (state.selectedLogIndex > 0) {
137769
+ controller.setSelectedLogIndex(state.selectedLogIndex - 1);
137770
+ }
137771
+ return;
137772
+ }
137773
+ if (key.downArrow || input === "j" || input === "J") {
137774
+ if (state.selectedLogIndex < filteredEntries.length - 1) {
137775
+ controller.setSelectedLogIndex(state.selectedLogIndex + 1);
137776
+ }
137777
+ return;
137778
+ }
137779
+ if (key.home) {
137780
+ controller.setSelectedLogIndex(0);
137781
+ return;
137782
+ }
137783
+ if (key.end) {
137784
+ controller.setSelectedLogIndex(Math.max(0, filteredEntries.length - 1));
137785
+ return;
137786
+ }
137787
+ }
137788
+ });
137789
+ if (!state.systemInfo) {
137790
+ return /* @__PURE__ */ jsx(Box, { flexDirection: "column", height: rows, children: /* @__PURE__ */ jsx(SplashScreen, { loadingStatus: state.loadingStatus }) });
137791
+ }
137792
+ const isNarrow = cols < 80 || rows < 20;
137793
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", height: rows, children: [
137794
+ state.mode === "interactive" ? /* @__PURE__ */ jsx(InteractiveMode, { state }) : isNarrow ? /* @__PURE__ */ jsx(StatusModeSingle, { state, controller }) : /* @__PURE__ */ jsx(StatusModeGrid, { state, rows, controller }),
137795
+ state.showHelp && /* @__PURE__ */ jsx(Box, { position: "absolute", marginTop: 3, marginLeft: 4, children: /* @__PURE__ */ jsx(HelpOverlay, {}) })
137796
+ ] });
137797
+ }
137798
+ var LOGO_COLORS, SPLASH_MIN_COLS, SPLASH_MIN_ROWS, PREFIX_WIDTH, PANEL_ORDER, KANBAN_COLUMNS, COLUMN_COLORS, SETTING_DEFS;
137799
+ var init_app = __esm({
137800
+ "src/commands/dashboard-tui/app.tsx"() {
137801
+ "use strict";
137802
+ init_state();
137803
+ init_logo();
137804
+ init_use_projects();
137805
+ LOGO_COLORS = ["whiteBright", "cyanBright", "cyan", "blueBright", "blue", "blue"];
137806
+ SPLASH_MIN_COLS = 56;
137807
+ SPLASH_MIN_ROWS = 20;
137808
+ PREFIX_WIDTH = 14;
137809
+ PANEL_ORDER = ["system", "logs", "utilities", "stats", "settings"];
137810
+ KANBAN_COLUMNS = ["todo", "in-progress", "in-review", "done"];
137811
+ COLUMN_COLORS = {
137812
+ todo: "yellow",
137813
+ "in-progress": "cyan",
137814
+ "in-review": "magenta",
137815
+ done: "green"
137816
+ };
137817
+ SETTING_DEFS = [
137818
+ { key: "maxConcurrent", label: "Max Concurrent", type: "number" },
137819
+ { key: "maxWorktrees", label: "Max Worktrees", type: "number" },
137820
+ { key: "autoMerge", label: "Auto Merge", type: "boolean" },
137821
+ { key: "mergeStrategy", label: "Merge Strategy", type: "enum", options: ["direct", "squash", "rebase"] },
137822
+ { key: "pollIntervalMs", label: "Poll Interval (ms)", type: "number" },
137823
+ { key: "enginePaused", label: "Engine Paused", type: "boolean" },
137824
+ { key: "globalPause", label: "Global Pause", type: "boolean" }
137825
+ ];
137826
+ }
137827
+ });
137828
+
137829
+ // src/commands/dashboard-tui/controller.ts
137830
+ var DashboardTUI;
137831
+ var init_controller = __esm({
137832
+ "src/commands/dashboard-tui/controller.ts"() {
137833
+ "use strict";
137834
+ init_log_ring_buffer();
137835
+ init_state();
136296
137836
  DashboardTUI = class {
136297
- activeSection = "system";
137837
+ // State fields mirror the original private layout so tests can access them.
137838
+ activeSection = "logs";
137839
+ // Named `logBuffer` to match what captureConsole tests access via
137840
+ // `(tui as unknown as { logBuffer: LogRingBuffer }).logBuffer`.
136298
137841
  logBuffer;
136299
137842
  systemInfo = null;
136300
137843
  taskStats = null;
136301
137844
  settings = null;
136302
137845
  callbacks = null;
136303
137846
  isRunning = false;
136304
- rl = null;
136305
- originalHandlers = /* @__PURE__ */ new Map();
136306
- lastRenderHeight = 0;
136307
137847
  showHelp = false;
136308
- uptimeTimer = null;
136309
- resizeHandler = null;
136310
- // Logs interaction state
136311
- selectedLogIndex = 0;
136312
- logsViewportStart = 0;
137848
+ logsSeverityFilter = "all";
136313
137849
  logsWrapEnabled = false;
136314
137850
  logsExpandedMode = false;
136315
- logsSeverityFilter = "all";
137851
+ selectedLogIndex = 0;
137852
+ logsViewportStart = 0;
137853
+ loadingStatus = "Starting\u2026";
137854
+ mode = "status";
137855
+ interactiveData = null;
137856
+ interactiveView = "board";
137857
+ // Subscribers registered by the Ink App component.
137858
+ subscribers = /* @__PURE__ */ new Set();
137859
+ // Cached snapshot — useSyncExternalStore compares by Object.is, so we must
137860
+ // return the same reference between renders unless state actually changed.
137861
+ // notify() invalidates this; getSnapshot() rebuilds on demand.
137862
+ cachedSnapshot = null;
137863
+ // Ink instance — set when start() is called.
137864
+ inkInstance = null;
137865
+ // Uptime ticker to keep footer time live.
137866
+ uptimeTimer = null;
136316
137867
  constructor() {
136317
137868
  this.logBuffer = new LogRingBuffer();
136318
137869
  }
136319
- // ── Public API ─────────────────────────────────────────────────────────────
136320
- /** Returns whether the TUI is currently running */
137870
+ // ── Subscription API (for Ink App) ────────────────────────────────────────
137871
+ subscribe(callback) {
137872
+ this.subscribers.add(callback);
137873
+ return () => this.subscribers.delete(callback);
137874
+ }
137875
+ getSnapshot() {
137876
+ if (this.cachedSnapshot) return this.cachedSnapshot;
137877
+ this.cachedSnapshot = {
137878
+ activeSection: this.activeSection,
137879
+ logEntries: this.logBuffer.getAll(),
137880
+ systemInfo: this.systemInfo,
137881
+ taskStats: this.taskStats,
137882
+ settings: this.settings,
137883
+ callbacks: this.callbacks,
137884
+ showHelp: this.showHelp,
137885
+ logsSeverityFilter: this.logsSeverityFilter,
137886
+ logsWrapEnabled: this.logsWrapEnabled,
137887
+ logsExpandedMode: this.logsExpandedMode,
137888
+ selectedLogIndex: this.selectedLogIndex,
137889
+ logsViewportStart: this.logsViewportStart,
137890
+ loadingStatus: this.loadingStatus,
137891
+ mode: this.mode,
137892
+ interactiveData: this.interactiveData,
137893
+ interactiveView: this.interactiveView
137894
+ };
137895
+ return this.cachedSnapshot;
137896
+ }
137897
+ notify() {
137898
+ this.cachedSnapshot = null;
137899
+ for (const cb of this.subscribers) cb();
137900
+ }
137901
+ // ── Public API (unchanged from original DashboardTUI) ─────────────────────
136321
137902
  get running() {
136322
137903
  return this.isRunning;
136323
137904
  }
136324
137905
  setCallbacks(callbacks) {
136325
137906
  this.callbacks = callbacks;
137907
+ this.notify();
136326
137908
  }
136327
137909
  setSystemInfo(info) {
136328
137910
  this.systemInfo = info;
136329
- this.render();
137911
+ this.notify();
136330
137912
  }
136331
137913
  setTaskStats(stats) {
136332
137914
  this.taskStats = stats;
136333
- this.render();
137915
+ this.notify();
136334
137916
  }
136335
137917
  setSettings(settings) {
136336
137918
  this.settings = settings;
136337
- this.render();
137919
+ this.notify();
137920
+ }
137921
+ setLoadingStatus(text) {
137922
+ this.loadingStatus = text;
137923
+ this.notify();
137924
+ }
137925
+ setInteractiveData(data) {
137926
+ this.interactiveData = data;
137927
+ this.notify();
137928
+ }
137929
+ setInteractiveView(view) {
137930
+ this.interactiveView = view;
137931
+ this.notify();
136338
137932
  }
136339
137933
  addLog(entry) {
136340
- this.logBuffer.push({
136341
- ...entry,
136342
- timestamp: /* @__PURE__ */ new Date()
136343
- });
136344
- this.clampSelectedLogIndex(this.getFilteredLogEntries());
136345
- this.render();
137934
+ const beforeCount = this.getFilteredLogEntries().length;
137935
+ const wasAtTail = beforeCount === 0 || this.selectedLogIndex === beforeCount - 1;
137936
+ this.logBuffer.push({ ...entry, timestamp: /* @__PURE__ */ new Date() });
137937
+ const after = this.getFilteredLogEntries();
137938
+ if (wasAtTail) {
137939
+ this.selectedLogIndex = Math.max(0, after.length - 1);
137940
+ } else {
137941
+ this.clampSelectedLogIndex(after);
137942
+ }
137943
+ this.notify();
136346
137944
  }
136347
- /**
136348
- * Clear logs and reset selection state.
136349
- */
136350
137945
  clearLogs() {
136351
137946
  this.logBuffer.clear();
136352
137947
  this.selectedLogIndex = 0;
136353
137948
  this.logsViewportStart = 0;
136354
137949
  this.logsExpandedMode = false;
137950
+ this.notify();
136355
137951
  }
136356
137952
  log(message, prefix) {
136357
137953
  this.addLog({ level: "info", message, prefix });
@@ -136362,251 +137958,56 @@ var init_dashboard_tui = __esm({
136362
137958
  error(message, prefix) {
136363
137959
  this.addLog({ level: "error", message, prefix });
136364
137960
  }
136365
- async start() {
136366
- if (this.isRunning) return;
136367
- this.isRunning = true;
136368
- enableAlternateScreen();
136369
- hideCursor();
136370
- this.saveSignalHandlers();
136371
- this.rl = readline.createInterface({
136372
- input: process.stdin,
136373
- output: process.stdout,
136374
- terminal: true
136375
- });
136376
- process.stdin.setRawMode?.(true);
136377
- process.stdin.resume();
136378
- process.stdin.setEncoding("utf8");
136379
- readline.emitKeypressEvents(process.stdin);
136380
- process.stdin.on("keypress", (str, key) => {
136381
- if (str) {
136382
- this.handleKeypress(str);
136383
- } else if (key.ctrl && key.name === "c") {
136384
- this.handleKeypress("");
136385
- } else if (key.name === "return" || key.name === "enter") {
136386
- this.handleKeypress("\r");
136387
- } else if (key.name === "right") {
136388
- this.handleKeypress("\x1B[C");
136389
- } else if (key.name === "left") {
136390
- this.handleKeypress("\x1B[D");
136391
- } else if (key.name === "up") {
136392
- this.handleKeypress("\x1B[A");
136393
- } else if (key.name === "down") {
136394
- this.handleKeypress("\x1B[B");
136395
- } else if (key.name === "escape") {
136396
- this.handleKeypress("\x1B");
136397
- } else if (key.name === "home") {
136398
- this.handleKeypress("Home");
136399
- } else if (key.name === "end") {
136400
- this.handleKeypress("End");
136401
- } else if (key.name === "space") {
136402
- this.handleKeypress(" ");
136403
- }
136404
- });
136405
- this.uptimeTimer = setInterval(() => {
136406
- if (this.isRunning) {
136407
- this.renderFooter();
136408
- }
136409
- }, 5e3);
136410
- this.resizeHandler = () => {
136411
- if (this.isRunning) {
136412
- this.render();
136413
- }
136414
- };
136415
- process.stdout.on("resize", this.resizeHandler);
136416
- this.render();
136417
- }
136418
- async stop() {
136419
- if (!this.isRunning) return;
136420
- this.isRunning = false;
136421
- if (this.uptimeTimer) {
136422
- clearInterval(this.uptimeTimer);
136423
- this.uptimeTimer = null;
136424
- }
136425
- if (this.resizeHandler) {
136426
- process.stdout.off("resize", this.resizeHandler);
136427
- this.resizeHandler = null;
136428
- }
136429
- this.restoreTerminal();
136430
- this.restoreSignalHandlers();
136431
- if (this.rl) {
136432
- this.rl.close();
136433
- this.rl = null;
136434
- }
136435
- }
136436
- // ── Private: Signal Handling ───────────────────────────────────────────────
136437
- saveSignalHandlers() {
136438
- const signals = ["SIGINT", "SIGTERM", "SIGHUP"];
136439
- for (const sig of signals) {
136440
- const listeners = process.listeners(sig);
136441
- if (listeners.length > 0) {
136442
- this.originalHandlers.set(sig, listeners[listeners.length - 1]);
136443
- }
136444
- }
137961
+ // ── State helpers called from Ink App ────────────────────────────────────
137962
+ setActiveSection(section) {
137963
+ this.activeSection = section;
137964
+ this.showHelp = false;
137965
+ this.notify();
136445
137966
  }
136446
- restoreSignalHandlers() {
136447
- for (const [sig, handler] of this.originalHandlers) {
136448
- process.on(sig, handler);
136449
- }
136450
- this.originalHandlers.clear();
137967
+ setShowHelp(show) {
137968
+ this.showHelp = show;
137969
+ this.notify();
136451
137970
  }
136452
- // ── Private: Terminal Restoration ────────────────────────────────────────
136453
- restoreTerminal() {
136454
- showCursor();
136455
- disableAlternateScreen();
136456
- process.stdout.write("\n");
136457
- process.stdin.pause?.();
136458
- process.stdin.setRawMode?.(false);
137971
+ setLogsWrapEnabled(enabled) {
137972
+ this.logsWrapEnabled = enabled;
137973
+ this.notify();
136459
137974
  }
136460
- // ── Private: Key Handling ────────────────────────────────────────────────
136461
- handleKeypress(key) {
136462
- if (key === "") {
136463
- void this.stop();
136464
- process.exit(0);
136465
- return;
136466
- }
136467
- if (key === "q" || key === "Q") {
136468
- void this.stop();
136469
- process.exit(0);
136470
- return;
136471
- }
136472
- if (key === "?" || key === "h" || key === "H") {
136473
- this.showHelp = !this.showHelp;
136474
- this.render();
136475
- return;
136476
- }
136477
- if (key >= "1" && key <= "5") {
136478
- const index2 = parseInt(key, 10) - 1;
136479
- if (index2 >= 0 && index2 < SECTION_ORDER.length) {
136480
- this.activeSection = SECTION_ORDER[index2];
136481
- this.showHelp = false;
136482
- this.render();
136483
- }
136484
- return;
136485
- }
136486
- if (key === "\x1B[C" || key === "n" || key === "N") {
136487
- const currentIndex = SECTION_ORDER.indexOf(this.activeSection);
136488
- this.activeSection = SECTION_ORDER[(currentIndex + 1) % SECTION_ORDER.length];
136489
- this.showHelp = false;
136490
- this.render();
136491
- return;
136492
- }
136493
- if (key === "\x1B[D" || key === "p" || key === "P") {
136494
- const currentIndex = SECTION_ORDER.indexOf(this.activeSection);
136495
- this.activeSection = SECTION_ORDER[(currentIndex - 1 + SECTION_ORDER.length) % SECTION_ORDER.length];
136496
- this.showHelp = false;
136497
- this.render();
136498
- return;
136499
- }
136500
- if (this.activeSection === "utilities") {
136501
- this.handleUtilityKeypress(key);
136502
- return;
136503
- }
136504
- if (this.activeSection === "logs") {
136505
- this.handleLogsKeypress(key);
136506
- return;
136507
- }
137975
+ setLogsExpandedMode(expanded) {
137976
+ this.logsExpandedMode = expanded;
137977
+ this.notify();
136508
137978
  }
136509
- handleLogsKeypress(key) {
137979
+ setSelectedLogIndex(index2) {
136510
137980
  const entries = this.getFilteredLogEntries();
136511
- const maxIndex = Math.max(0, entries.length - 1);
136512
- if (key === "\x1B") {
136513
- if (this.logsExpandedMode) {
136514
- this.logsExpandedMode = false;
136515
- this.showHelp = false;
136516
- this.render();
136517
- } else if (this.showHelp) {
136518
- this.showHelp = false;
136519
- this.render();
136520
- }
136521
- return;
136522
- }
136523
- if (key === "\r") {
136524
- if (entries.length === 0) {
136525
- return;
136526
- }
136527
- this.logsExpandedMode = !this.logsExpandedMode;
136528
- this.render();
136529
- return;
136530
- }
136531
- if (key === "w" || key === "W") {
136532
- this.logsWrapEnabled = !this.logsWrapEnabled;
136533
- this.render();
136534
- return;
136535
- }
136536
- if (key === "f" || key === "F") {
136537
- this.cycleLogsSeverityFilter();
136538
- this.clampSelectedLogIndex(this.getFilteredLogEntries());
136539
- this.logsViewportStart = 0;
136540
- this.render();
136541
- return;
136542
- }
136543
- if (key === "\x1B[A" || key === "k" || key === "K") {
136544
- if (entries.length === 0) return;
136545
- if (this.selectedLogIndex > 0) {
136546
- this.selectedLogIndex--;
136547
- this.render();
136548
- }
136549
- return;
136550
- }
136551
- if (key === "\x1B[B" || key === "j" || key === "J") {
136552
- if (entries.length === 0) return;
136553
- if (this.selectedLogIndex < maxIndex) {
136554
- this.selectedLogIndex++;
136555
- this.render();
136556
- }
136557
- return;
136558
- }
136559
- if (key === "Home") {
136560
- if (entries.length === 0) return;
136561
- if (this.selectedLogIndex !== 0) {
136562
- this.selectedLogIndex = 0;
136563
- this.render();
136564
- }
136565
- return;
136566
- }
136567
- if (key === "End") {
136568
- if (entries.length === 0) return;
136569
- if (this.selectedLogIndex !== maxIndex) {
136570
- this.selectedLogIndex = maxIndex;
136571
- this.render();
136572
- }
136573
- return;
136574
- }
136575
- if (key === " " || key === "e" || key === "E") {
136576
- if (entries.length === 0) {
136577
- return;
136578
- }
136579
- this.logsExpandedMode = !this.logsExpandedMode;
136580
- this.render();
136581
- return;
136582
- }
137981
+ this.selectedLogIndex = this.clampIndex(index2, entries.length);
137982
+ this.notify();
136583
137983
  }
136584
- getFilteredLogEntries() {
136585
- const entries = this.logBuffer.getAll();
136586
- if (this.logsSeverityFilter === "all") {
136587
- return entries;
136588
- }
136589
- return entries.filter((entry) => entry.level === this.logsSeverityFilter);
137984
+ setLogsViewportStart(start) {
137985
+ this.logsViewportStart = start;
137986
+ this.notify();
136590
137987
  }
136591
- clampSelectedLogIndex(entries) {
136592
- if (entries.length === 0) {
136593
- this.selectedLogIndex = 0;
136594
- this.logsExpandedMode = false;
136595
- return;
136596
- }
136597
- if (this.selectedLogIndex >= entries.length) {
136598
- this.selectedLogIndex = entries.length - 1;
136599
- }
136600
- if (this.selectedLogIndex < 0) {
136601
- this.selectedLogIndex = 0;
136602
- }
137988
+ setMode(mode) {
137989
+ this.mode = mode;
137990
+ this.notify();
136603
137991
  }
136604
- cycleLogsSeverityFilter() {
137992
+ cycleSection(direction) {
137993
+ const idx = SECTION_ORDER.indexOf(this.activeSection);
137994
+ this.activeSection = SECTION_ORDER[(idx + direction + SECTION_ORDER.length) % SECTION_ORDER.length];
137995
+ this.showHelp = false;
137996
+ this.notify();
137997
+ }
137998
+ cycleSeverityFilter() {
136605
137999
  const order = ["all", "info", "warn", "error"];
136606
- const currentIndex = order.indexOf(this.logsSeverityFilter);
136607
- this.logsSeverityFilter = order[(currentIndex + 1) % order.length];
138000
+ const idx = order.indexOf(this.logsSeverityFilter);
138001
+ this.logsSeverityFilter = order[(idx + 1) % order.length];
138002
+ this.clampSelectedLogIndex(this.getFilteredLogEntries());
138003
+ this.logsViewportStart = 0;
138004
+ this.notify();
136608
138005
  }
136609
- async handleUtilityKeypress(key) {
138006
+ getFilteredLogEntries() {
138007
+ const all = this.logBuffer.getAll();
138008
+ return this.logsSeverityFilter === "all" ? all : all.filter((e) => e.level === this.logsSeverityFilter);
138009
+ }
138010
+ async handleUtilityAction(key) {
136610
138011
  if (!this.callbacks) return;
136611
138012
  switch (key.toLowerCase()) {
136612
138013
  case "r":
@@ -136627,517 +138028,83 @@ var init_dashboard_tui = __esm({
136627
138028
  break;
136628
138029
  }
136629
138030
  }
136630
- // ── Private: Rendering ───────────────────────────────────────────────────
136631
- render() {
136632
- if (!this.isRunning) return;
136633
- clearScreen();
136634
- moveCursorTo(1, 1);
136635
- this.renderHeader();
136636
- this.renderSection();
136637
- this.renderFooter();
136638
- if (this.showHelp) {
136639
- this.renderHelpOverlay();
136640
- }
136641
- }
136642
- renderHeader() {
136643
- const cols = process.stdout.columns || 80;
136644
- const title = colorize(" fusion ", "cyan");
136645
- const titleLen = visibleLength(title);
136646
- process.stdout.write(title);
136647
- if (cols >= 70) {
136648
- for (let i = 0; i < SECTION_ORDER.length; i++) {
136649
- const section = SECTION_ORDER[i];
136650
- const isActive = section === this.activeSection;
136651
- const num = (i + 1).toString();
136652
- const label = section.charAt(0).toUpperCase() + section.slice(1);
136653
- const tabText = `[${num}] ${label}`;
136654
- const style = isActive ? "brightBlue" : "dim";
136655
- process.stdout.write(colorize(` ${tabText} `, style));
136656
- }
136657
- } else if (cols >= 40) {
136658
- const shortLabels = {
136659
- logs: "L",
136660
- system: "S",
136661
- utilities: "U",
136662
- stats: "St",
136663
- settings: "Se"
136664
- };
136665
- for (let i = 0; i < SECTION_ORDER.length; i++) {
136666
- const section = SECTION_ORDER[i];
136667
- const isActive = section === this.activeSection;
136668
- const num = (i + 1).toString();
136669
- const shortLabel = shortLabels[section];
136670
- const tabText = `[${num}]${shortLabel}`;
136671
- const style = isActive ? "brightBlue" : "dim";
136672
- process.stdout.write(colorize(` ${tabText} `, style));
136673
- }
136674
- } else {
136675
- const activeIndex = SECTION_ORDER.indexOf(this.activeSection);
136676
- const activeLabel = this.activeSection.charAt(0).toUpperCase() + this.activeSection.slice(1);
136677
- process.stdout.write(colorize(` [${activeIndex + 1}]${activeLabel} `, "brightBlue"));
136678
- process.stdout.write(colorize(" [n/p]nav ", "dim"));
136679
- }
136680
- const tabsLength = SECTION_ORDER.reduce((acc, s, i) => {
136681
- let label;
136682
- if (cols >= 70) {
136683
- label = s.charAt(0).toUpperCase() + s.slice(1);
136684
- } else if (cols >= 40) {
136685
- const shortLabels = {
136686
- logs: "L",
136687
- system: "S",
136688
- utilities: "U",
136689
- stats: "St",
136690
- settings: "Se"
136691
- };
136692
- label = shortLabels[s];
136693
- } else {
136694
- label = s.charAt(0).toUpperCase() + s.slice(1);
136695
- }
136696
- const tabText = `[${i + 1}]${label} `;
136697
- return acc + tabText.length;
136698
- }, 0);
136699
- const headerLen = titleLen + tabsLength;
136700
- const remaining = cols - headerLen;
136701
- if (remaining > 0) {
136702
- process.stdout.write(" ".repeat(remaining));
136703
- }
136704
- process.stdout.write("\n");
136705
- process.stdout.write(colorize("\u2500".repeat(Math.max(20, cols)), "dim") + "\n");
136706
- }
136707
- renderSection() {
136708
- switch (this.activeSection) {
136709
- case "logs":
136710
- this.renderLogsSection();
136711
- break;
136712
- case "system":
136713
- this.renderSystemSection();
136714
- break;
136715
- case "utilities":
136716
- this.renderUtilitiesSection();
136717
- break;
136718
- case "stats":
136719
- this.renderStatsSection();
136720
- break;
136721
- case "settings":
136722
- this.renderSettingsSection();
136723
- break;
136724
- }
136725
- }
136726
- getFooterTopRow(totalRows) {
136727
- return Math.max(1, totalRows - 2);
136728
- }
136729
- getLogsListRowBudget(totalRows) {
136730
- const firstLogBodyRow = 8;
136731
- const footerTopRow = this.getFooterTopRow(totalRows);
136732
- return Math.max(0, footerTopRow - firstLogBodyRow);
138031
+ // ── Lifecycle ──────────────────────────────────────────────────────────────
138032
+ async start() {
138033
+ if (this.isRunning) return;
138034
+ this.isRunning = true;
138035
+ const { render } = await import("ink");
138036
+ const { createElement } = await import("react");
138037
+ const { DashboardApp: DashboardApp2 } = await Promise.resolve().then(() => (init_app(), app_exports));
138038
+ this.inkInstance = render(
138039
+ createElement(DashboardApp2, { controller: this })
138040
+ );
138041
+ this.uptimeTimer = setInterval(() => {
138042
+ if (this.isRunning) this.notify();
138043
+ }, 5e3);
136733
138044
  }
136734
- getLogEntryRowCount(entry, cols) {
136735
- if (!this.logsWrapEnabled) {
136736
- return 1;
138045
+ async stop() {
138046
+ if (!this.isRunning) return;
138047
+ this.isRunning = false;
138048
+ if (this.uptimeTimer) {
138049
+ clearInterval(this.uptimeTimer);
138050
+ this.uptimeTimer = null;
136737
138051
  }
136738
- const prefixLen = 30;
136739
- const availableWidth = Math.max(8, cols - prefixLen);
136740
- return Math.max(1, this.wrapText(entry.message, availableWidth).length);
136741
- }
136742
- getLogsViewportWindow(entries, rowBudget, cols) {
136743
- if (entries.length === 0) {
136744
- this.logsViewportStart = 0;
136745
- return { start: 0, end: 0 };
136746
- }
136747
- const maxStart = Math.max(0, entries.length - 1);
136748
- const safeSelectedIndex = Math.max(0, Math.min(this.selectedLogIndex, entries.length - 1));
136749
- let start = Math.max(0, Math.min(this.logsViewportStart, maxStart));
136750
- if (safeSelectedIndex < start) {
136751
- start = safeSelectedIndex;
136752
- }
136753
- const measureWindow = (startIndex) => {
136754
- let rowsUsed = 0;
136755
- let end = startIndex;
136756
- while (end < entries.length) {
136757
- const entryRows = this.getLogEntryRowCount(entries[end], cols);
136758
- if (end > startIndex && rowsUsed + entryRows > rowBudget) {
136759
- break;
136760
- }
136761
- rowsUsed += entryRows;
136762
- end++;
136763
- if (rowsUsed >= rowBudget) {
136764
- break;
136765
- }
136766
- }
136767
- return { end };
136768
- };
136769
- let window2 = measureWindow(start);
136770
- while (safeSelectedIndex >= window2.end && start < maxStart) {
136771
- start++;
136772
- window2 = measureWindow(start);
138052
+ if (this.inkInstance) {
138053
+ this.inkInstance.unmount();
138054
+ this.inkInstance = null;
136773
138055
  }
136774
- this.logsViewportStart = start;
136775
- return { start, end: window2.end };
136776
138056
  }
136777
- renderLogsSection() {
136778
- const cols = process.stdout.columns || 80;
136779
- const rows = process.stdout.rows ?? 38;
136780
- const allEntries = this.logBuffer.getAll();
136781
- const entries = this.getFilteredLogEntries();
136782
- const rowBudget = this.getLogsListRowBudget(rows);
136783
- process.stdout.write(colorize("\n LOGS\n", "bold"));
136784
- process.stdout.write(colorize(` Ring buffer: ${this.logBuffer.total}/${MAX_LOG_ENTRIES} entries
136785
- `, "dim"));
136786
- if (allEntries.length === 0) {
136787
- process.stdout.write(colorize(" No log entries yet.\n", "dim"));
136788
- return;
136789
- }
138057
+ // ── Private helpers ────────────────────────────────────────────────────────
138058
+ clampSelectedLogIndex(entries) {
136790
138059
  if (entries.length === 0) {
136791
- const filterLabel = this.logsSeverityFilter.toUpperCase();
136792
- process.stdout.write(colorize(` No log entries match filter ${filterLabel}.
136793
- `, "dim"));
136794
- process.stdout.write(colorize(" Press [f] to cycle severity filter.\n", "dim"));
136795
- return;
136796
- }
136797
- const safeSelectedIndex = Math.min(this.selectedLogIndex, Math.max(0, entries.length - 1));
136798
- if (safeSelectedIndex !== this.selectedLogIndex) {
136799
- this.selectedLogIndex = safeSelectedIndex;
136800
- }
136801
- if (this.logsExpandedMode) {
136802
- this.renderLogsExpandedPane(entries[safeSelectedIndex], safeSelectedIndex, entries.length);
136803
- return;
136804
- }
136805
- const wrapIndicator = this.logsWrapEnabled ? colorize(" [w] wrap on", "dim") : colorize(" [w] wrap off", "dim");
136806
- const filterIndicator = colorize(` [f] filter ${this.logsSeverityFilter}`, "dim");
136807
- process.stdout.write(`${wrapIndicator}${filterIndicator}
136808
-
136809
- `);
136810
- if (rowBudget === 0) {
136811
- process.stdout.write(colorize(" Terminal too short \u2014 expand terminal to view logs.\n", "dim"));
136812
- return;
136813
- }
136814
- const { start: startIndex, end: endIndex } = this.getLogsViewportWindow(entries, rowBudget, cols);
136815
- const visibleEntries = entries.slice(startIndex, endIndex);
136816
- const visibleReversed = [...visibleEntries].reverse();
136817
- const selectedDisplayIndex = safeSelectedIndex >= startIndex && safeSelectedIndex < endIndex ? visibleEntries.length - 1 - (safeSelectedIndex - startIndex) : -1;
136818
- const prefixLen = 30;
136819
- const availableWidth = Math.max(8, cols - prefixLen);
136820
- let remainingRows = rowBudget;
136821
- for (let displayIdx = 0; displayIdx < visibleReversed.length && remainingRows > 0; displayIdx++) {
136822
- const entry = visibleReversed[displayIdx];
136823
- const isSelected = displayIdx === selectedDisplayIndex;
136824
- const selector = isSelected ? colorize("\u25B8 ", "brightGreen") : " ";
136825
- const ts = colorize(formatTimestamp3(entry.timestamp), "dim");
136826
- const prefix = entry.prefix ? colorize(`[${entry.prefix}]`, "gray") : "";
136827
- const levelChar = entry.level === "error" ? colorize("\u2717", "brightRed") : entry.level === "warn" ? colorize("\u26A0", "brightYellow") : colorize("\u2713", "brightGreen");
136828
- if (this.logsWrapEnabled) {
136829
- const wrappedLines = this.wrapText(entry.message, availableWidth);
136830
- const lineBudget = Math.max(1, remainingRows);
136831
- const renderedLines = wrappedLines.slice(0, lineBudget);
136832
- const firstLine = `${selector}${ts} ${levelChar} ${prefix ? prefix + " " : ""}${renderedLines[0] ?? ""}`;
136833
- process.stdout.write(visibleTruncate(firstLine, cols - 1) + "\n");
136834
- remainingRows--;
136835
- for (let i = 1; i < renderedLines.length && remainingRows > 0; i++) {
136836
- const continuation = ` ${renderedLines[i]}`;
136837
- process.stdout.write(visibleTruncate(continuation, cols - 1) + "\n");
136838
- remainingRows--;
136839
- }
136840
- } else {
136841
- const messageWidth = Math.max(8, cols - prefixLen);
136842
- const message = visibleTruncate(entry.message, messageWidth);
136843
- const line = `${selector}${ts} ${levelChar} ${prefix ? prefix + " " : ""}${message}`;
136844
- process.stdout.write(visibleTruncate(line, cols - 1) + "\n");
136845
- remainingRows--;
136846
- }
136847
- }
136848
- }
136849
- /**
136850
- * Render the expanded log entry detail pane.
136851
- * Replaces the normal list view with a focused view of a single entry.
136852
- */
136853
- renderLogsExpandedPane(entry, index2, total) {
136854
- const cols = process.stdout.columns || 80;
136855
- const rows = process.stdout.rows ?? 24;
136856
- const maxContentRows = Math.max(1, rows - 12);
136857
- process.stdout.write(colorize(" EXPANDED LOG ENTRY\n", "bold"));
136858
- const navHint = colorize(` Entry ${index2 + 1} of ${total} | [\u2191/k] older [\u2193/j] newer [Enter/Esc] close
136859
- `, "dim");
136860
- process.stdout.write(navHint);
136861
- process.stdout.write(colorize(" " + "\u2500".repeat(Math.max(20, cols - 4)) + "\n", "dim"));
136862
- const ts = formatTimestamp3(entry.timestamp);
136863
- const levelLabel = entry.level === "error" ? colorize("ERROR", "brightRed") : entry.level === "warn" ? colorize("WARN", "brightYellow") : colorize("INFO", "brightGreen");
136864
- process.stdout.write(colorize(` Timestamp: `, "gray") + colorize(ts, "white") + "\n");
136865
- process.stdout.write(colorize(` Level: `, "gray") + levelLabel + "\n");
136866
- if (entry.prefix) {
136867
- process.stdout.write(colorize(` Prefix: `, "gray") + colorize(entry.prefix, "dim") + "\n");
136868
- }
136869
- process.stdout.write("\n");
136870
- process.stdout.write(colorize(" MESSAGE\n", "bold"));
136871
- const messageIndent = " ";
136872
- const availableWidth = Math.max(8, cols - messageIndent.length);
136873
- const wrappedMessage = this.wrapText(entry.message, availableWidth);
136874
- let linesPrinted = 5;
136875
- for (const line of wrappedMessage) {
136876
- if (linesPrinted >= maxContentRows) {
136877
- process.stdout.write(colorize(`
136878
- ... (truncated)
136879
- `, "dim"));
136880
- break;
136881
- }
136882
- process.stdout.write(messageIndent + line + "\n");
136883
- linesPrinted++;
136884
- }
136885
- const footerHint = colorize(`
136886
- [Esc] or [Enter] to close expanded view
136887
- `, "dim");
136888
- process.stdout.write(footerHint);
136889
- }
136890
- /**
136891
- * Wrap text to fit within available width, returning an array of lines.
136892
- * Respects ANSI escape sequences via visibleLength.
136893
- */
136894
- wrapText(text, maxWidth) {
136895
- if (maxWidth <= 0) return [""];
136896
- if (visibleLength(text) <= maxWidth) return [text];
136897
- const lines = [];
136898
- let remaining = text;
136899
- while (visibleLength(remaining) > maxWidth) {
136900
- let breakIdx = 0;
136901
- for (let i = 0; i < remaining.length; i++) {
136902
- const char = remaining[i];
136903
- if (char === " " || char === " ") {
136904
- if (visibleLength(remaining.substring(0, i)) <= maxWidth) {
136905
- breakIdx = i;
136906
- }
136907
- }
136908
- if (visibleLength(remaining.substring(0, i + 1)) > maxWidth) {
136909
- break;
136910
- }
136911
- }
136912
- if (breakIdx === 0) {
136913
- const firstTokenMatch = remaining.match(/^(\S+)/);
136914
- if (firstTokenMatch) {
136915
- const firstToken = firstTokenMatch[1];
136916
- if (visibleLength(firstToken) > maxWidth) {
136917
- const chunkSize = Math.max(1, maxWidth - 1);
136918
- let chunkStart = 0;
136919
- while (chunkStart < firstToken.length) {
136920
- const chunk = firstToken.substring(chunkStart, chunkStart + chunkSize);
136921
- lines.push(chunk);
136922
- chunkStart += chunkSize;
136923
- }
136924
- remaining = remaining.substring(firstToken.length).trimStart();
136925
- continue;
136926
- }
136927
- }
136928
- breakIdx = Math.min(maxWidth, remaining.length);
136929
- }
136930
- lines.push(remaining.substring(0, breakIdx).trimEnd());
136931
- remaining = remaining.substring(breakIdx).trimStart();
136932
- }
136933
- if (remaining.length > 0) {
136934
- lines.push(remaining);
136935
- }
136936
- return lines.length > 0 ? lines : [""];
136937
- }
136938
- renderSystemSection() {
136939
- if (!this.systemInfo) {
136940
- process.stdout.write(colorize("\n System information not available.\n", "dim"));
136941
- return;
136942
- }
136943
- const cols = process.stdout.columns || 80;
136944
- const info = this.systemInfo;
136945
- const rows = [];
136946
- rows.push(colorize("\n SYSTEM INFORMATION\n", "bold"));
136947
- rows.push("");
136948
- const labelWidth = 12;
136949
- const availableValueWidth = Math.max(8, cols - labelWidth - 1);
136950
- rows.push(` ${colorize("Host:", "white")} ${info.host}`);
136951
- rows.push(` ${colorize("Port:", "white")} ${info.port}`);
136952
- rows.push(` ${colorize("URL:", "white")} ${colorize(visibleTruncate(info.baseUrl, availableValueWidth), "brightCyan")}`);
136953
- rows.push("");
136954
- if (info.authEnabled) {
136955
- rows.push(` ${colorize("Auth:", "white")} ${colorize("bearer token required", "yellow")}`);
136956
- if (info.authToken) {
136957
- rows.push(` ${colorize("Token:", "white")} ${visibleTruncate(info.authToken, availableValueWidth)}`);
136958
- }
136959
- if (info.tokenizedUrl) {
136960
- rows.push(` ${colorize("Open:", "white")} ${visibleTruncate(info.tokenizedUrl, availableValueWidth)}`);
136961
- rows.push(colorize(" (browser stores token, click once)", "dim"));
136962
- }
136963
- } else {
136964
- rows.push(` ${colorize("Auth:", "white")} ${colorize("disabled (--no-auth)", "dim")}`);
136965
- }
136966
- rows.push("");
136967
- rows.push(` ${colorize("AI Engine:", "white")} ${info.engineMode === "dev" ? colorize("\u2717 disabled (dev mode)", "yellow") : info.engineMode === "paused" ? colorize("\u23F8 paused", "brightYellow") : colorize("\u2713 active", "brightGreen")}`);
136968
- rows.push(` ${colorize("File Watcher:", "white")} ${info.fileWatcher ? colorize("\u2713 active", "brightGreen") : colorize("\u2717 inactive", "brightRed")}`);
136969
- rows.push(` ${colorize("Uptime:", "white")} ${formatUptime(Date.now() - info.startTimeMs)}`);
136970
- for (const row of rows) {
136971
- process.stdout.write(visibleTruncate(row, cols) + "\n");
136972
- }
136973
- }
136974
- renderUtilitiesSection() {
136975
- const cols = process.stdout.columns || 80;
136976
- const actions = [
136977
- { id: "refresh", label: "Refresh Stats", key: "r", description: "Re-fetch task and agent counts" },
136978
- { id: "clear", label: "Clear Logs", key: "c", description: "Clear the log ring buffer" },
136979
- { id: "pause", label: "Toggle Engine Pause", key: "t", description: "Pause/resume AI engine automation" },
136980
- { id: "help", label: "Help", key: "?", description: "Show keyboard shortcuts" }
136981
- ];
136982
- process.stdout.write(colorize("\n UTILITIES\n", "bold"));
136983
- process.stdout.write(colorize(" Press key to execute action\n\n", "dim"));
136984
- const prefixWidth = 2 + 3 + 1 + 20 + 3;
136985
- const descriptionWidth = Math.max(8, cols - prefixWidth - 1);
136986
- for (const action of actions) {
136987
- const keyDisplay = colorize(`[${action.key}]`, "brightYellow");
136988
- const label = colorize(action.label.padEnd(20), "white");
136989
- const description = visibleTruncate(action.description, descriptionWidth);
136990
- const line = ` ${keyDisplay} ${label} - ${description}`;
136991
- process.stdout.write(visibleTruncate(line, cols - 1) + "\n");
136992
- }
136993
- }
136994
- renderStatsSection() {
136995
- const cols = process.stdout.columns || 80;
136996
- if (!this.taskStats) {
136997
- process.stdout.write(colorize("\n Statistics not available.\n", "dim"));
136998
- return;
136999
- }
137000
- const stats = this.taskStats;
137001
- const rows = [];
137002
- rows.push(colorize("\n STATISTICS\n", "bold"));
137003
- rows.push("");
137004
- rows.push(` ${colorize("Total Tasks:", "white")} ${stats.total}`);
137005
- rows.push("");
137006
- rows.push(` ${colorize("By Column:", "dim")}`);
137007
- for (const [column, count] of Object.entries(stats.byColumn)) {
137008
- const colName = column.replace("-", " ").replace(/\b\w/g, (l) => l.toUpperCase());
137009
- const activeMark = (column === "in-progress" || column === "in-review") && count > 0 ? colorize(" \u25CF", "brightGreen") : "";
137010
- rows.push(` ${colName}: ${count}${activeMark}`);
137011
- }
137012
- rows.push("");
137013
- rows.push(` ${colorize("Active Tasks:", "white")} ${stats.active} (in-progress + in-review)`);
137014
- rows.push("");
137015
- rows.push(` ${colorize("Agents:", "dim")}`);
137016
- rows.push(` Idle: ${stats.agents.idle}`);
137017
- rows.push(` Active: ${stats.agents.active}`);
137018
- rows.push(` Running: ${stats.agents.running}`);
137019
- rows.push(` Error: ${stats.agents.error}`);
137020
- for (const row of rows) {
137021
- process.stdout.write(visibleTruncate(row, cols) + "\n");
137022
- }
137023
- }
137024
- renderSettingsSection() {
137025
- const cols = process.stdout.columns || 80;
137026
- if (!this.settings) {
137027
- process.stdout.write(colorize("\n Settings not available.\n", "dim"));
138060
+ this.selectedLogIndex = 0;
138061
+ this.logsExpandedMode = false;
137028
138062
  return;
137029
138063
  }
137030
- const s = this.settings;
137031
- const rows = [];
137032
- rows.push(colorize("\n SETTINGS\n", "bold"));
137033
- rows.push("");
137034
- const settingsList = [
137035
- ["maxConcurrent", s.maxConcurrent.toString()],
137036
- ["maxWorktrees", s.maxWorktrees.toString()],
137037
- ["autoMerge", s.autoMerge ? "enabled" : "disabled"],
137038
- ["mergeStrategy", s.mergeStrategy],
137039
- ["pollIntervalMs", `${s.pollIntervalMs}ms`],
137040
- ["enginePaused", s.enginePaused ? "yes" : "no"],
137041
- ["globalPause", s.globalPause ? "yes" : "no"]
137042
- ];
137043
- const keyWidth = Math.max(...settingsList.map(([k]) => k.length));
137044
- for (const [key, value] of settingsList) {
137045
- const keyPad = key.padEnd(keyWidth);
137046
- const isEnabled = value === "enabled" || value === "yes";
137047
- const isDisabled = value === "disabled" || value === "no";
137048
- const valueColor = isEnabled ? "brightGreen" : isDisabled ? "brightYellow" : "white";
137049
- rows.push(` ${colorize(keyPad, "gray")} ${colorize(value, valueColor)}`);
138064
+ if (this.selectedLogIndex >= entries.length) {
138065
+ this.selectedLogIndex = entries.length - 1;
137050
138066
  }
137051
- for (const row of rows) {
137052
- process.stdout.write(visibleTruncate(row, cols) + "\n");
137053
- }
137054
- }
137055
- renderFooter() {
137056
- const cols = process.stdout.columns || 80;
137057
- const footerY = Math.max(1, (process.stdout.rows ?? 22) - 2);
137058
- moveCursorTo(1, footerY);
137059
- clearLine();
137060
- const status = this.systemInfo ? `${this.systemInfo.baseUrl} | ${formatUptime(Date.now() - this.systemInfo.startTimeMs)}` : "";
137061
- const left = colorize("Press ? for help", "dim");
137062
- const right = colorize(visibleTruncate(status, Math.max(20, cols - 20)), "dim");
137063
- const leftLen = visibleLength(left);
137064
- const rightLen = visibleLength(right);
137065
- process.stdout.write(left);
137066
- const padding = Math.max(1, cols - leftLen - rightLen - 2);
137067
- process.stdout.write(" ".repeat(padding));
137068
- process.stdout.write(right);
137069
- process.stdout.write("\n");
137070
- }
137071
- /**
137072
- * Build help overlay lines as an array of strings.
137073
- * Uses dynamic visibleLength calculation to ensure consistent box width.
137074
- * All box-drawing rows (including borders) have the same total visible width.
137075
- *
137076
- * @param boxWidth - The interior content width (excluding the two box characters)
137077
- * @param useBoxDrawing - Whether to use box-drawing characters (true) or compact text (false)
137078
- * @returns Array of help lines
137079
- */
137080
- buildHelpLines(boxWidth, useBoxDrawing) {
137081
- if (useBoxDrawing) {
137082
- const boxRow = (content) => {
137083
- const padding = Math.max(0, boxWidth - visibleLength(content));
137084
- return "\u2502" + content + " ".repeat(padding) + "\u2502";
137085
- };
137086
- return [
137087
- "\u250C" + "\u2500".repeat(boxWidth) + "\u2510",
137088
- boxRow(centerText("KEYBOARD SHORTCUTS", boxWidth, " ")),
137089
- "\u251C" + "\u2500".repeat(boxWidth) + "\u2524",
137090
- boxRow(" [1-5] Switch to tab by number"),
137091
- boxRow(" [n] / \u2192 Next tab"),
137092
- boxRow(" [p] / \u2190 Previous tab"),
137093
- boxRow(" [r] Refresh stats (Utilities)"),
137094
- boxRow(" [c] Clear logs (Utilities)"),
137095
- boxRow(" [t] Toggle engine pause (Utilities)"),
137096
- boxRow(" [\u2191/\u2193/k/j] Navigate log entries (Logs)"),
137097
- boxRow(" [Home/End] First/last log entry (Logs)"),
137098
- boxRow(" [Enter/Space/e] Expand log (Logs)"),
137099
- boxRow(" [w] Toggle word wrap (Logs)"),
137100
- boxRow(" [f] Cycle severity filter (Logs)"),
137101
- boxRow(" [?] / [h] Toggle help"),
137102
- boxRow(" [q] Quit"),
137103
- boxRow(" [Ctrl+C] Force quit"),
137104
- "\u2514" + "\u2500".repeat(boxWidth) + "\u2518"
137105
- ];
137106
- } else {
137107
- return [
137108
- "KEYBOARD SHORTCUTS",
137109
- " [1-5] Switch tab | [n/p] Next/Prev | [q] Quit",
137110
- " [\u2191\u2193/k/j] Navigate logs | [Home/End] First/Last (Logs)",
137111
- " [Enter/Space/e] Expand log | [w] Toggle wrap (Logs)",
137112
- " [f] Cycle severity filter (Logs)",
137113
- " [r] Refresh | [c] Clear logs | [t] Toggle engine",
137114
- " [?/h] Help | [Ctrl+C] Force quit"
137115
- ];
138067
+ if (this.selectedLogIndex < 0) {
138068
+ this.selectedLogIndex = 0;
137116
138069
  }
137117
138070
  }
137118
- renderHelpOverlay() {
137119
- const cols = process.stdout.columns || 80;
137120
- const rows = process.stdout.rows || 24;
137121
- const boxWidth = Math.min(62, Math.max(cols - 4, 20));
137122
- const useBoxDrawing = cols >= boxWidth + 4;
137123
- const rawHelpLines = this.buildHelpLines(boxWidth, useBoxDrawing);
137124
- const compactBoxWidth = useBoxDrawing ? boxWidth : Math.max(...rawHelpLines.map(visibleLength));
137125
- const boxHeight = rawHelpLines.length;
137126
- const safeStartX = Math.max(1, Math.floor((cols - compactBoxWidth) / 2));
137127
- const safeStartY = Math.max(1, Math.floor((rows - boxHeight) / 2));
137128
- const clearTop = Math.max(1, safeStartY - 1);
137129
- const clearBottom = Math.min(rows, safeStartY + boxHeight);
137130
- for (let y = clearTop; y <= clearBottom; y++) {
137131
- moveCursorTo(1, y);
137132
- clearLine();
137133
- }
137134
- for (let i = 0; i < rawHelpLines.length; i++) {
137135
- const color = i === 0 || i === 2 || i === rawHelpLines.length - 1 ? "brightBlue" : "white";
137136
- moveCursorTo(safeStartX, safeStartY + i);
137137
- process.stdout.write(colorize(rawHelpLines[i], color));
137138
- }
138071
+ clampIndex(index2, length) {
138072
+ if (length === 0) return 0;
138073
+ return Math.max(0, Math.min(index2, length - 1));
137139
138074
  }
137140
138075
  };
138076
+ }
138077
+ });
138078
+
138079
+ // src/commands/dashboard-tui/log-sink.ts
138080
+ function formatConsoleArgs(args, fallbackLevel = "info") {
138081
+ const stringified = args.map((arg) => {
138082
+ if (typeof arg === "string") return arg;
138083
+ if (arg instanceof Error) return arg.stack ?? arg.message;
138084
+ if (arg === null || arg === void 0) return String(arg);
138085
+ if (typeof arg === "object") {
138086
+ try {
138087
+ return JSON.stringify(arg);
138088
+ } catch {
138089
+ return String(arg);
138090
+ }
138091
+ }
138092
+ return String(arg);
138093
+ }).join(" ");
138094
+ const markerMatch = stringified.match(LOG_LEVEL_MARKER_REGEX);
138095
+ const level = markerMatch?.[1];
138096
+ const withoutMarker = markerMatch ? stringified.replace(LOG_LEVEL_MARKER_REGEX, "") : stringified;
138097
+ const match = withoutMarker.match(/^\[([^\]]+)\]\s*(.*)$/s);
138098
+ if (match) {
138099
+ return { prefix: match[1], message: match[2], level: level ?? fallbackLevel };
138100
+ }
138101
+ return { message: withoutMarker, level: level ?? fallbackLevel };
138102
+ }
138103
+ var LOG_LEVEL_MARKER_REGEX, DashboardLogSink;
138104
+ var init_log_sink = __esm({
138105
+ "src/commands/dashboard-tui/log-sink.ts"() {
138106
+ "use strict";
138107
+ LOG_LEVEL_MARKER_REGEX = /^\u0000fnlvl=(info|warn|error)\u0000\s*/;
137141
138108
  DashboardLogSink = class {
137142
138109
  tui = null;
137143
138110
  isTTY;
@@ -137180,19 +138147,6 @@ var init_dashboard_tui = __esm({
137180
138147
  console.error(line);
137181
138148
  }
137182
138149
  }
137183
- /**
137184
- * Monkey-patch `console.log/warn/error` so everything (including the engine's
137185
- * createLogger() output, which writes directly to console.error) surfaces in
137186
- * the TUI's log ring buffer. Structured logger calls carry an internal
137187
- * severity marker so `logger.log(...)` still lands as info even when routed
137188
- * through console.error transport. Without capture, most runtime logs render
137189
- * beneath the alt-screen TUI and are immediately overwritten on the next
137190
- * render, leaving the Logs tab nearly empty.
137191
- *
137192
- * Messages that start with `[prefix] rest` are unpacked so the TUI stores
137193
- * `prefix="prefix"` and `message="rest"`. Idempotent; call `releaseConsole()`
137194
- * on TUI shutdown to restore the originals.
137195
- */
137196
138150
  captureConsole() {
137197
138151
  if (this.originalConsole) return;
137198
138152
  this.originalConsole = {
@@ -137213,7 +138167,6 @@ var init_dashboard_tui = __esm({
137213
138167
  this.writeCapturedConsoleLog(level, message, prefix);
137214
138168
  };
137215
138169
  }
137216
- /** Restore console.log/warn/error to their pre-capture implementations. */
137217
138170
  releaseConsole() {
137218
138171
  if (!this.originalConsole) return;
137219
138172
  console.log = this.originalConsole.log;
@@ -137233,7 +138186,27 @@ var init_dashboard_tui = __esm({
137233
138186
  this.log(message, prefix);
137234
138187
  }
137235
138188
  };
137236
- LOG_LEVEL_MARKER_REGEX = /^\u0000fnlvl=(info|warn|error)\u0000\s*/;
138189
+ }
138190
+ });
138191
+
138192
+ // src/commands/dashboard-tui/utils.ts
138193
+ function isTTYAvailable() {
138194
+ return Boolean(process.stdout.isTTY && process.stdin.isTTY);
138195
+ }
138196
+ var init_utils = __esm({
138197
+ "src/commands/dashboard-tui/utils.ts"() {
138198
+ "use strict";
138199
+ }
138200
+ });
138201
+
138202
+ // src/commands/dashboard-tui/index.ts
138203
+ var init_dashboard_tui = __esm({
138204
+ "src/commands/dashboard-tui/index.ts"() {
138205
+ "use strict";
138206
+ init_controller();
138207
+ init_log_sink();
138208
+ init_log_ring_buffer();
138209
+ init_utils();
137237
138210
  }
137238
138211
  });
137239
138212
 
@@ -137473,6 +138446,7 @@ async function runDashboard(port, opts = {}) {
137473
138446
  }
137474
138447
  });
137475
138448
  await tui.start();
138449
+ tui.setLoadingStatus("Initializing task store\u2026");
137476
138450
  logSink.setTUI(tui);
137477
138451
  logSink.captureConsole();
137478
138452
  }
@@ -137593,8 +138567,10 @@ async function runDashboard(port, opts = {}) {
137593
138567
  }
137594
138568
  const automationStore = new AutomationStore(cwd);
137595
138569
  await automationStore.init();
138570
+ if (tui) tui.setLoadingStatus("Initializing agent store\u2026");
137596
138571
  agentStore = new AgentStore({ rootDir: store.getFusionDir() });
137597
138572
  await agentStore.init();
138573
+ if (tui) tui.setLoadingStatus("Starting engine\u2026");
137598
138574
  if (tui && isTTY) {
137599
138575
  registerHandler(store, "task:created", scheduleStatsRefresh);
137600
138576
  registerHandler(store, "task:moved", scheduleStatsRefresh);
@@ -138203,6 +139179,120 @@ async function runDashboard(port, opts = {}) {
138203
139179
  active,
138204
139180
  agents: agentStats
138205
139181
  });
139182
+ if (centralCoreForMesh) {
139183
+ const centralCore = centralCoreForMesh;
139184
+ const projectStores = /* @__PURE__ */ new Map();
139185
+ tui.setInteractiveData({
139186
+ listProjects: async () => {
139187
+ const projects = await centralCore.listProjects();
139188
+ return projects.map((p) => ({ id: p.id, name: p.name, path: p.path }));
139189
+ },
139190
+ listTasks: async (projectPath) => {
139191
+ let projectStore = projectStores.get(projectPath);
139192
+ if (!projectStore) {
139193
+ projectStore = projectPath === cwd ? store : new TaskStore(projectPath);
139194
+ if (projectPath !== cwd) await projectStore.init();
139195
+ projectStores.set(projectPath, projectStore);
139196
+ }
139197
+ const tasks2 = await projectStore.listTasks({ slim: true, includeArchived: false });
139198
+ return tasks2.map((t) => ({
139199
+ id: t.id,
139200
+ title: t.title,
139201
+ description: t.description ?? "",
139202
+ column: t.column,
139203
+ agentState: t.agentState
139204
+ }));
139205
+ },
139206
+ createTask: async (projectPath, input) => {
139207
+ let projectStore = projectStores.get(projectPath);
139208
+ if (!projectStore) {
139209
+ projectStore = projectPath === cwd ? store : new TaskStore(projectPath);
139210
+ if (projectPath !== cwd) await projectStore.init();
139211
+ projectStores.set(projectPath, projectStore);
139212
+ }
139213
+ const created = await projectStore.createTask({
139214
+ title: input.title,
139215
+ description: input.description ?? input.title
139216
+ });
139217
+ return {
139218
+ id: created.id,
139219
+ title: created.title,
139220
+ description: created.description ?? "",
139221
+ column: created.column,
139222
+ agentState: created.agentState
139223
+ };
139224
+ },
139225
+ listAgents: async () => {
139226
+ const list = await agentStore.listAgents();
139227
+ return list.map((a) => ({
139228
+ id: a.id,
139229
+ name: a.name,
139230
+ state: a.state,
139231
+ role: a.role,
139232
+ taskId: a.taskId,
139233
+ lastHeartbeatAt: a.lastHeartbeatAt
139234
+ }));
139235
+ },
139236
+ getAgentDetail: async (id) => {
139237
+ const d = await agentStore.getAgentDetail(id, 10);
139238
+ if (!d) return null;
139239
+ return {
139240
+ id: d.id,
139241
+ name: d.name,
139242
+ state: d.state,
139243
+ role: d.role,
139244
+ taskId: d.taskId,
139245
+ lastHeartbeatAt: d.lastHeartbeatAt,
139246
+ title: d.title,
139247
+ capabilities: [d.role],
139248
+ recentRuns: d.completedRuns.slice(0, 10).map((r) => ({
139249
+ id: r.id,
139250
+ startedAt: r.startedAt,
139251
+ endedAt: r.endedAt,
139252
+ status: r.status,
139253
+ triggerDetail: r.triggerDetail
139254
+ }))
139255
+ };
139256
+ },
139257
+ updateAgentState: async (id, state) => {
139258
+ await agentStore.updateAgentState(id, state);
139259
+ },
139260
+ deleteAgent: async (id) => {
139261
+ await agentStore.deleteAgent(id);
139262
+ },
139263
+ getSettings: async () => {
139264
+ const s = await store.getSettings();
139265
+ return {
139266
+ maxConcurrent: s.maxConcurrent ?? 1,
139267
+ maxWorktrees: s.maxWorktrees ?? 2,
139268
+ autoMerge: s.autoMerge ?? false,
139269
+ mergeStrategy: s.mergeStrategy ?? "direct",
139270
+ pollIntervalMs: s.pollIntervalMs ?? 6e4,
139271
+ enginePaused: s.enginePaused ?? false,
139272
+ globalPause: s.globalPause ?? false
139273
+ };
139274
+ },
139275
+ updateSettings: async (partial) => {
139276
+ const mapped = {};
139277
+ if (partial.maxConcurrent !== void 0) mapped.maxConcurrent = partial.maxConcurrent;
139278
+ if (partial.maxWorktrees !== void 0) mapped.maxWorktrees = partial.maxWorktrees;
139279
+ if (partial.autoMerge !== void 0) mapped.autoMerge = partial.autoMerge;
139280
+ if (partial.mergeStrategy !== void 0) mapped.mergeStrategy = partial.mergeStrategy;
139281
+ if (partial.pollIntervalMs !== void 0) mapped.pollIntervalMs = partial.pollIntervalMs;
139282
+ if (partial.enginePaused !== void 0) mapped.enginePaused = partial.enginePaused;
139283
+ if (partial.globalPause !== void 0) mapped.globalPause = partial.globalPause;
139284
+ await store.updateSettings(mapped);
139285
+ },
139286
+ listModels: () => {
139287
+ return modelRegistry.getAll().map((m) => ({
139288
+ id: m.id,
139289
+ name: m.name,
139290
+ provider: m.provider ?? "unknown",
139291
+ contextWindow: m.contextWindow ?? 0
139292
+ }));
139293
+ }
139294
+ });
139295
+ }
138206
139296
  tui.log(`Dashboard started at ${baseUrl}`);
138207
139297
  if (engineMode === "active") {
138208
139298
  tui.log("AI engine active");
@@ -138342,7 +139432,7 @@ __export(node_exports, {
138342
139432
  runNodeRemove: () => runNodeRemove,
138343
139433
  runNodeShow: () => runNodeShow
138344
139434
  });
138345
- import { createInterface as createInterface3 } from "node:readline/promises";
139435
+ import { createInterface as createInterface2 } from "node:readline/promises";
138346
139436
  function maskApiKey(key) {
138347
139437
  if (!key) return "none";
138348
139438
  if (key.length < 4) return "****";
@@ -138537,7 +139627,7 @@ async function runNodeDisconnect(name, options = {}) {
138537
139627
  process.exit(1);
138538
139628
  }
138539
139629
  if (!options.force) {
138540
- const rl = createInterface3({ input: process.stdin, output: process.stdout });
139630
+ const rl = createInterface2({ input: process.stdin, output: process.stdout });
138541
139631
  const answer = await rl.question(`Disconnect node '${node.name}'? [y/N] `);
138542
139632
  rl.close();
138543
139633
  if (answer.trim().toLowerCase() !== "y") {
@@ -139766,13 +140856,13 @@ var desktop_exports = {};
139766
140856
  __export(desktop_exports, {
139767
140857
  runDesktop: () => runDesktop
139768
140858
  });
139769
- import { spawn as spawn5 } from "node:child_process";
140859
+ import { spawn as spawn6 } from "node:child_process";
139770
140860
  import { once as once2 } from "node:events";
139771
140861
  import { join as join51 } from "node:path";
139772
140862
  import { createRequire as createRequire3 } from "node:module";
139773
140863
  function runCommand(command, args, cwd) {
139774
140864
  return new Promise((resolve31, reject2) => {
139775
- const child = spawn5(command, args, {
140865
+ const child = spawn6(command, args, {
139776
140866
  cwd,
139777
140867
  stdio: "inherit",
139778
140868
  env: process.env
@@ -139857,7 +140947,7 @@ async function runDesktop(options = {}) {
139857
140947
  electronEnv.FUSION_DASHBOARD_URL = process.env.FUSION_DASHBOARD_URL ?? "http://localhost:5173";
139858
140948
  electronEnv.NODE_ENV = "development";
139859
140949
  }
139860
- const electronProcess = spawn5(electronBinary, electronArgs, {
140950
+ const electronProcess = spawn6(electronBinary, electronArgs, {
139861
140951
  cwd: rootDir,
139862
140952
  stdio: "inherit",
139863
140953
  env: electronEnv
@@ -139929,7 +141019,7 @@ __export(task_exports, {
139929
141019
  runTaskUnpause: () => runTaskUnpause,
139930
141020
  runTaskUpdate: () => runTaskUpdate
139931
141021
  });
139932
- import { createInterface as createInterface4 } from "node:readline/promises";
141022
+ import { createInterface as createInterface3 } from "node:readline/promises";
139933
141023
  import { watchFile, unwatchFile, statSync as statSync6, existsSync as existsSync37, readFileSync as readFileSync14 } from "node:fs";
139934
141024
  import { join as join52 } from "node:path";
139935
141025
  function asLocalProjectContext(store) {
@@ -139998,7 +141088,7 @@ async function runTaskCreate(descriptionArg, attachFiles, depends, projectName)
139998
141088
  let description = descriptionArg;
139999
141089
  const projectContext = await getProjectContext(projectName);
140000
141090
  if (!description) {
140001
- const rl = createInterface4({ input: process.stdin, output: process.stdout });
141091
+ const rl = createInterface3({ input: process.stdin, output: process.stdout });
140002
141092
  description = await rl.question("Task description: ");
140003
141093
  rl.close();
140004
141094
  }
@@ -140334,7 +141424,7 @@ async function runTaskRefine(id, feedbackArg, projectName) {
140334
141424
  const store = await getStore(projectName);
140335
141425
  let feedback = feedbackArg;
140336
141426
  if (feedback === void 0) {
140337
- const rl = createInterface4({ input: process.stdin, output: process.stdout });
141427
+ const rl = createInterface3({ input: process.stdin, output: process.stdout });
140338
141428
  feedback = await rl.question("What needs to be refined? ");
140339
141429
  rl.close();
140340
141430
  }
@@ -140405,7 +141495,7 @@ async function runTaskDelete(id, force, projectName) {
140405
141495
  return;
140406
141496
  }
140407
141497
  if (!force) {
140408
- const rl = createInterface4({ input: process.stdin, output: process.stdout });
141498
+ const rl = createInterface3({ input: process.stdin, output: process.stdout });
140409
141499
  const answer = await rl.question(`Are you sure you want to delete ${id}? [y/N] `);
140410
141500
  rl.close();
140411
141501
  const trimmed = answer.trim().toLowerCase();
@@ -140469,7 +141559,7 @@ async function runTaskImportGitHubInteractive(ownerRepo, options = {}, projectNa
140469
141559
  console.log(` ${i + 1}. #${issue.number} ${issue.title.slice(0, 80)}${issue.title.length > 80 ? "\u2026" : ""}${status}`);
140470
141560
  }
140471
141561
  console.log();
140472
- const rl = createInterface4({ input: process.stdin, output: process.stdout });
141562
+ const rl = createInterface3({ input: process.stdin, output: process.stdout });
140473
141563
  let selectedIndices = [];
140474
141564
  let validInput = false;
140475
141565
  while (!validInput) {
@@ -140612,7 +141702,7 @@ async function runTaskComment(id, message, author = "user", projectName) {
140612
141702
  const store = await getStore(projectName);
140613
141703
  let text = message;
140614
141704
  if (text === void 0) {
140615
- const rl = createInterface4({ input: process.stdin, output: process.stdout });
141705
+ const rl = createInterface3({ input: process.stdin, output: process.stdout });
140616
141706
  text = await rl.question("Comment: ");
140617
141707
  rl.close();
140618
141708
  }
@@ -140655,7 +141745,7 @@ async function runTaskSteer(id, message, projectName) {
140655
141745
  const store = await getStore(projectName);
140656
141746
  let text = message;
140657
141747
  if (text === void 0) {
140658
- const rl = createInterface4({ input: process.stdin, output: process.stdout });
141748
+ const rl = createInterface3({ input: process.stdin, output: process.stdout });
140659
141749
  text = await rl.question("Message: ");
140660
141750
  rl.close();
140661
141751
  }
@@ -140786,7 +141876,7 @@ async function promptText(question) {
140786
141876
  console.log(` ${question.description}`);
140787
141877
  }
140788
141878
  console.log(" (Enter your response. Type DONE on its own line when finished):\n");
140789
- const rl = createInterface4({ input: process.stdin, output: process.stdout });
141879
+ const rl = createInterface3({ input: process.stdin, output: process.stdout });
140790
141880
  const lines = [];
140791
141881
  return new Promise((resolve31) => {
140792
141882
  const askLine = () => {
@@ -140820,7 +141910,7 @@ async function promptSingleSelect(question) {
140820
141910
  console.log(` ${opt.description}`);
140821
141911
  }
140822
141912
  }
140823
- const rl = createInterface4({ input: process.stdin, output: process.stdout });
141913
+ const rl = createInterface3({ input: process.stdin, output: process.stdout });
140824
141914
  while (true) {
140825
141915
  const answer = await rl.question("\n Select (1-" + question.options.length + "): ");
140826
141916
  const num = parseInt(answer.trim(), 10);
@@ -140848,7 +141938,7 @@ async function promptMultiSelect(question) {
140848
141938
  console.log(` ${opt.description}`);
140849
141939
  }
140850
141940
  }
140851
- const rl = createInterface4({ input: process.stdin, output: process.stdout });
141941
+ const rl = createInterface3({ input: process.stdin, output: process.stdout });
140852
141942
  while (true) {
140853
141943
  const answer = await rl.question("\n Select (comma-separated): ");
140854
141944
  const nums = answer.split(",").map((s) => parseInt(s.trim(), 10)).filter((n) => !isNaN(n));
@@ -140871,7 +141961,7 @@ async function promptConfirm(question) {
140871
141961
  if (question.description) {
140872
141962
  console.log(` ${question.description}`);
140873
141963
  }
140874
- const rl = createInterface4({ input: process.stdin, output: process.stdout });
141964
+ const rl = createInterface3({ input: process.stdin, output: process.stdout });
140875
141965
  const answer = await rl.question("\n [Y/n]: ");
140876
141966
  rl.close();
140877
141967
  const trimmed = answer.trim().toLowerCase();
@@ -140936,7 +142026,7 @@ function wrapText(text, width) {
140936
142026
  async function runTaskPlan(initialPlanArg, yesFlag = false, projectName) {
140937
142027
  let initialPlan = initialPlanArg;
140938
142028
  if (!initialPlan) {
140939
- const rl = createInterface4({ input: process.stdin, output: process.stdout });
142029
+ const rl = createInterface3({ input: process.stdin, output: process.stdout });
140940
142030
  console.log("\n Let's plan your task. What would you like to accomplish?\n");
140941
142031
  initialPlan = await rl.question(" Describe your idea: ");
140942
142032
  rl.close();
@@ -141037,7 +142127,7 @@ async function runTaskPlan(initialPlanArg, yesFlag = false, projectName) {
141037
142127
  displaySummary(result.data);
141038
142128
  let confirmed = yesFlag;
141039
142129
  if (!yesFlag) {
141040
- const rl = createInterface4({ input: process.stdin, output: process.stdout });
142130
+ const rl = createInterface3({ input: process.stdin, output: process.stdout });
141041
142131
  const answer = await rl.question(" Create this task? [Y/n]: ");
141042
142132
  rl.close();
141043
142133
  const trimmed = answer.trim().toLowerCase();
@@ -141521,7 +142611,7 @@ __export(git_exports, {
141521
142611
  });
141522
142612
  import { exec as exec10 } from "node:child_process";
141523
142613
  import { promisify as promisify12 } from "node:util";
141524
- import { createInterface as createInterface5 } from "node:readline/promises";
142614
+ import { createInterface as createInterface4 } from "node:readline/promises";
141525
142615
  async function resolveGitCwd(projectName) {
141526
142616
  if (projectName) {
141527
142617
  return (await resolveProject(projectName)).projectPath;
@@ -141709,7 +142799,7 @@ async function runGitPull(options = {}) {
141709
142799
  console.log();
141710
142800
  console.log(" \u26A0 Warning: You have uncommitted changes.");
141711
142801
  console.log(` Branch: ${status.branch}`);
141712
- const rl = createInterface5({ input: process.stdin, output: process.stdout });
142802
+ const rl = createInterface4({ input: process.stdin, output: process.stdout });
141713
142803
  const answer = await rl.question(" Continue with pull? [y/N] ");
141714
142804
  rl.close();
141715
142805
  const trimmed = answer.trim().toLowerCase();
@@ -141760,7 +142850,7 @@ async function runGitPush(options = {}) {
141760
142850
  }
141761
142851
  if (!options.skipConfirm) {
141762
142852
  console.log();
141763
- const rl = createInterface5({ input: process.stdin, output: process.stdout });
142853
+ const rl = createInterface4({ input: process.stdin, output: process.stdout });
141764
142854
  const answer = await rl.question(` Push branch ${status.branch} to remote? [Y/n] `);
141765
142855
  rl.close();
141766
142856
  const trimmed = answer.trim().toLowerCase();
@@ -141897,7 +142987,7 @@ var init_backup2 = __esm({
141897
142987
  // src/project-resolver.ts
141898
142988
  import { existsSync as existsSync39, statSync as statSync7 } from "node:fs";
141899
142989
  import { dirname as dirname21, resolve as resolve25, normalize as normalize5 } from "node:path";
141900
- import { createInterface as createInterface6 } from "node:readline/promises";
142990
+ import { createInterface as createInterface5 } from "node:readline/promises";
141901
142991
  async function getCentralCore() {
141902
142992
  if (!centralCoreInstance) {
141903
142993
  centralCoreInstance = new CentralCore();
@@ -141928,7 +143018,7 @@ function findKbDir(startPath) {
141928
143018
  return null;
141929
143019
  }
141930
143020
  async function promptProjectSelection(projects, message = "Select a project:") {
141931
- const rl = createInterface6({ input: process.stdin, output: process.stdout });
143021
+ const rl = createInterface5({ input: process.stdin, output: process.stdout });
141932
143022
  console.log(`
141933
143023
  ${message}`);
141934
143024
  for (let i = 0; i < projects.length; i++) {
@@ -141945,7 +143035,7 @@ async function promptProjectSelection(projects, message = "Select a project:") {
141945
143035
  }
141946
143036
  }
141947
143037
  async function promptConfirm2(message, defaultYes = false) {
141948
- const rl = createInterface6({ input: process.stdin, output: process.stdout });
143038
+ const rl = createInterface5({ input: process.stdin, output: process.stdout });
141949
143039
  const prompt = defaultYes ? "[Y/n]" : "[y/N]";
141950
143040
  const answer = await rl.question(` ${message} ${prompt}: `);
141951
143041
  rl.close();
@@ -142005,7 +143095,7 @@ Run \`fn project remove ` + match.name + "` to clean up the registry entry.",
142005
143095
  Found fn project at ${fusionDir} but it's not registered.`);
142006
143096
  const shouldRegister = await promptConfirm2("Register this project now?", true);
142007
143097
  if (shouldRegister) {
142008
- const rl = createInterface6({ input: process.stdin, output: process.stdout });
143098
+ const rl = createInterface5({ input: process.stdin, output: process.stdout });
142009
143099
  const defaultName = fusionDir.split("/").pop() || "unnamed";
142010
143100
  const name = await rl.question(` Project name [${defaultName}]: `);
142011
143101
  rl.close();
@@ -142167,12 +143257,12 @@ __export(mission_exports, {
142167
143257
  runMissionShow: () => runMissionShow,
142168
143258
  runSliceAdd: () => runSliceAdd
142169
143259
  });
142170
- import { createInterface as createInterface7 } from "node:readline/promises";
143260
+ import { createInterface as createInterface6 } from "node:readline/promises";
142171
143261
  async function promptForTitleAndDescription(titleArg, titlePrompt, descriptionPrompt) {
142172
143262
  let title = titleArg;
142173
143263
  let description;
142174
143264
  if (!title) {
142175
- const rl = createInterface7({ input: process.stdin, output: process.stdout });
143265
+ const rl = createInterface6({ input: process.stdin, output: process.stdout });
142176
143266
  title = await rl.question(titlePrompt);
142177
143267
  if (!title?.trim()) {
142178
143268
  rl.close();
@@ -142301,7 +143391,7 @@ async function runMissionDelete(id, force, projectName) {
142301
143391
  process.exit(1);
142302
143392
  }
142303
143393
  if (!force) {
142304
- const rl = createInterface7({ input: process.stdin, output: process.stdout });
143394
+ const rl = createInterface6({ input: process.stdin, output: process.stdout });
142305
143395
  const answer = await rl.question(`Are you sure you want to delete ${id}: "${mission.title}"? [y/N] `);
142306
143396
  rl.close();
142307
143397
  const trimmed = answer.trim().toLowerCase();
@@ -142402,7 +143492,7 @@ async function runFeatureAdd(sliceId, titleArg, descriptionArg, acceptanceCriter
142402
143492
  let description = descriptionArg?.trim() || void 0;
142403
143493
  let acceptanceCriteria = acceptanceCriteriaArg?.trim() || void 0;
142404
143494
  if (!title) {
142405
- const rl = createInterface7({ input: process.stdin, output: process.stdout });
143495
+ const rl = createInterface6({ input: process.stdin, output: process.stdout });
142406
143496
  title = await rl.question("Feature title: ");
142407
143497
  if (!title?.trim()) {
142408
143498
  rl.close();
@@ -142496,7 +143586,7 @@ __export(project_exports, {
142496
143586
  });
142497
143587
  import { resolve as resolve26, isAbsolute as isAbsolute13, relative as relative11, basename as basename13 } from "node:path";
142498
143588
  import { existsSync as existsSync40, statSync as statSync8 } from "node:fs";
142499
- import { createInterface as createInterface8 } from "node:readline/promises";
143589
+ import { createInterface as createInterface7 } from "node:readline/promises";
142500
143590
  function formatDisplayPath(projectPath) {
142501
143591
  const rel = relative11(process.cwd(), projectPath);
142502
143592
  if (rel && !rel.startsWith("..") && rel !== "") {
@@ -142617,7 +143707,7 @@ async function runProjectAdd(name, path4, options = {}) {
142617
143707
  let projectName = name;
142618
143708
  let projectPath = path4;
142619
143709
  if (!projectName || !projectPath || options.interactive) {
142620
- const rl = createInterface8({ input: process.stdin, output: process.stdout });
143710
+ const rl = createInterface7({ input: process.stdin, output: process.stdout });
142621
143711
  if (!projectPath) {
142622
143712
  const defaultPath = process.cwd();
142623
143713
  const pathInput = await rl.question(` Project path [${defaultPath}]: `);
@@ -142743,7 +143833,7 @@ async function runProjectRemove(name, options = {}) {
142743
143833
  process.exit(1);
142744
143834
  }
142745
143835
  if (!options.force) {
142746
- const rl = createInterface8({ input: process.stdin, output: process.stdout });
143836
+ const rl = createInterface7({ input: process.stdin, output: process.stdout });
142747
143837
  const answer = await rl.question(`Unregister project '${project.name}'? [y/N] `);
142748
143838
  rl.close();
142749
143839
  if (answer.trim().toLowerCase() !== "y") {
@@ -143805,7 +144895,7 @@ __export(plugin_exports, {
143805
144895
  import { existsSync as existsSync44 } from "node:fs";
143806
144896
  import { join as join56 } from "node:path";
143807
144897
  import { readFile as readFile23 } from "node:fs/promises";
143808
- import * as readline2 from "node:readline";
144898
+ import * as readline from "node:readline";
143809
144899
  async function getProjectPath6(projectName) {
143810
144900
  if (projectName) {
143811
144901
  const context = await resolveProject(projectName);
@@ -143945,7 +145035,7 @@ async function runPluginUninstall(id, options) {
143945
145035
  console.log(` This will stop and remove the plugin.`);
143946
145036
  console.log();
143947
145037
  const response = await new Promise((resolve31) => {
143948
- const rl = readline2.createInterface({
145038
+ const rl = readline.createInterface({
143949
145039
  input: process.stdin,
143950
145040
  output: process.stdout
143951
145041
  });
@@ -144216,7 +145306,7 @@ __export(skills_exports, {
144216
145306
  runSkillsSearch: () => runSkillsSearch,
144217
145307
  searchSkills: () => searchSkills
144218
145308
  });
144219
- import { spawn as spawn6 } from "node:child_process";
145309
+ import { spawn as spawn7 } from "node:child_process";
144220
145310
  async function searchSkills(query, limit = 10) {
144221
145311
  const url = `${SKILLS_API_BASE}/api/search?q=${encodeURIComponent(query)}&limit=${limit}`;
144222
145312
  try {
@@ -144294,7 +145384,7 @@ async function runSkillsInstall(args, options) {
144294
145384
  npxArgs.push("--skill", options.skill);
144295
145385
  }
144296
145386
  npxArgs.push("-y", "-a", "pi");
144297
- const child = spawn6("npx", npxArgs, {
145387
+ const child = spawn7("npx", npxArgs, {
144298
145388
  cwd: process.cwd(),
144299
145389
  stdio: "inherit"
144300
145390
  });
@@ -144628,6 +145718,7 @@ var HELP = `
144628
145718
  fn \u2014 AI-orchestrated task board
144629
145719
 
144630
145720
  Usage:
145721
+ fn Launch the dashboard (same as fn dashboard)
144631
145722
  fn init [opts] Initialize a new fn project in the current directory
144632
145723
  fn dashboard Start the board web UI
144633
145724
  fn dashboard --paused Start with automation paused
@@ -144791,10 +145882,15 @@ function getFlagValueNumber(args, flag) {
144791
145882
  }
144792
145883
  async function main() {
144793
145884
  const { cleanedArgs: args, projectName } = extractGlobalProjectFlag(process.argv.slice(2));
144794
- if (args.length === 0 || args.includes("--help") || args.includes("-h")) {
145885
+ if (args.includes("--help") || args.includes("-h")) {
144795
145886
  console.log(HELP);
144796
145887
  process.exit(0);
144797
145888
  }
145889
+ if (args.length === 0) {
145890
+ const { runDashboard: runDashboard3 } = await Promise.resolve().then(() => (init_dashboard(), dashboard_exports));
145891
+ await runDashboard3(4040);
145892
+ return;
145893
+ }
144798
145894
  const command = args[0];
144799
145895
  const {
144800
145896
  runDashboard: runDashboard2,