opencode-dux 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +452 -0
- package/dist/agents/descriptions.d.ts +6 -0
- package/dist/agents/designer.d.ts +2 -0
- package/dist/agents/explorer.d.ts +2 -0
- package/dist/agents/fixer.d.ts +2 -0
- package/dist/agents/index.d.ts +22 -0
- package/dist/agents/interpreter.d.ts +2 -0
- package/dist/agents/librarian.d.ts +2 -0
- package/dist/agents/oracle.d.ts +2 -0
- package/dist/agents/orchestrator.d.ts +27 -0
- package/dist/agents/overrides.d.ts +18 -0
- package/dist/agents/prompt-blocks.d.ts +97 -0
- package/dist/agents/steward.d.ts +3 -0
- package/dist/cli/config-io.d.ts +24 -0
- package/dist/cli/config-manager.d.ts +4 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +1006 -0
- package/dist/cli/install.d.ts +2 -0
- package/dist/cli/mcps.d.ts +13 -0
- package/dist/cli/model-key-normalization.d.ts +1 -0
- package/dist/cli/paths.d.ts +35 -0
- package/dist/cli/providers.d.ts +137 -0
- package/dist/cli/skills.d.ts +22 -0
- package/dist/cli/system.d.ts +5 -0
- package/dist/cli/types.d.ts +38 -0
- package/dist/config/constants.d.ts +12 -0
- package/dist/config/index.d.ts +4 -0
- package/dist/config/loader.d.ts +40 -0
- package/dist/config/runtime-preset.d.ts +12 -0
- package/dist/config/schema.d.ts +281 -0
- package/dist/config/utils.d.ts +10 -0
- package/dist/discovery/local/types.d.ts +79 -0
- package/dist/discovery/local.d.ts +73 -0
- package/dist/discovery/mcp-servers.d.ts +88 -0
- package/dist/discovery/skills.d.ts +94 -0
- package/dist/hooks/apply-patch/codec.d.ts +7 -0
- package/dist/hooks/apply-patch/errors.d.ts +25 -0
- package/dist/hooks/apply-patch/execution-context.d.ts +27 -0
- package/dist/hooks/apply-patch/index.d.ts +15 -0
- package/dist/hooks/apply-patch/matching.d.ts +26 -0
- package/dist/hooks/apply-patch/operations.d.ts +3 -0
- package/dist/hooks/apply-patch/patch.d.ts +2 -0
- package/dist/hooks/apply-patch/prepared-changes.d.ts +17 -0
- package/dist/hooks/apply-patch/resolution.d.ts +19 -0
- package/dist/hooks/apply-patch/rewrite.d.ts +7 -0
- package/dist/hooks/apply-patch/test-helpers.d.ts +6 -0
- package/dist/hooks/apply-patch/types.d.ts +80 -0
- package/dist/hooks/auto-update-checker/cache.d.ts +11 -0
- package/dist/hooks/auto-update-checker/checker.d.ts +32 -0
- package/dist/hooks/auto-update-checker/constants.d.ts +11 -0
- package/dist/hooks/auto-update-checker/index.d.ts +18 -0
- package/dist/hooks/auto-update-checker/types.d.ts +22 -0
- package/dist/hooks/chat-headers.d.ts +16 -0
- package/dist/hooks/context-pressure-reminder/index.d.ts +33 -0
- package/dist/hooks/delegate-task-retry/guidance.d.ts +2 -0
- package/dist/hooks/delegate-task-retry/hook.d.ts +8 -0
- package/dist/hooks/delegate-task-retry/index.d.ts +4 -0
- package/dist/hooks/delegate-task-retry/patterns.d.ts +11 -0
- package/dist/hooks/filter-available-skills/index.d.ts +32 -0
- package/dist/hooks/foreground-fallback/index.d.ts +72 -0
- package/dist/hooks/image-hook.d.ts +5 -0
- package/dist/hooks/index.d.ts +14 -0
- package/dist/hooks/json-error-recovery/hook.d.ts +18 -0
- package/dist/hooks/json-error-recovery/index.d.ts +1 -0
- package/dist/hooks/phase-reminder/index.d.ts +26 -0
- package/dist/hooks/post-file-tool-nudge/index.d.ts +19 -0
- package/dist/hooks/task-session-manager/index.d.ts +52 -0
- package/dist/hooks/todo-continuation/index.d.ts +53 -0
- package/dist/hooks/todo-continuation/todo-hygiene.d.ts +35 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +31782 -0
- package/dist/mcp/context7.d.ts +6 -0
- package/dist/mcp/grep-app.d.ts +6 -0
- package/dist/mcp/index.d.ts +13 -0
- package/dist/mcp/types.d.ts +12 -0
- package/dist/mcp/websearch.d.ts +9 -0
- package/dist/skills/registry.d.ts +29 -0
- package/dist/subscriptions/accounts-store.d.ts +57 -0
- package/dist/subscriptions/index.d.ts +13 -0
- package/dist/subscriptions/neuralwatt-scraper.d.ts +14 -0
- package/dist/subscriptions/opencode-go-scraper.d.ts +27 -0
- package/dist/subscriptions/types.d.ts +115 -0
- package/dist/subscriptions/usage-service.d.ts +74 -0
- package/dist/tools/ast-grep/cli.d.ts +15 -0
- package/dist/tools/ast-grep/constants.d.ts +25 -0
- package/dist/tools/ast-grep/downloader.d.ts +5 -0
- package/dist/tools/ast-grep/index.d.ts +10 -0
- package/dist/tools/ast-grep/tools.d.ts +3 -0
- package/dist/tools/ast-grep/types.d.ts +30 -0
- package/dist/tools/ast-grep/utils.d.ts +4 -0
- package/dist/tools/delegate.d.ts +14 -0
- package/dist/tools/index.d.ts +5 -0
- package/dist/tools/preset-manager.d.ts +27 -0
- package/dist/tools/smartfetch/binary.d.ts +3 -0
- package/dist/tools/smartfetch/cache.d.ts +6 -0
- package/dist/tools/smartfetch/constants.d.ts +12 -0
- package/dist/tools/smartfetch/index.d.ts +3 -0
- package/dist/tools/smartfetch/network.d.ts +38 -0
- package/dist/tools/smartfetch/secondary-model.d.ts +28 -0
- package/dist/tools/smartfetch/tool.d.ts +3 -0
- package/dist/tools/smartfetch/types.d.ts +122 -0
- package/dist/tools/smartfetch/utils.d.ts +18 -0
- package/dist/tui-state.d.ts +168 -0
- package/dist/tui.d.ts +37 -0
- package/dist/tui.js +1896 -0
- package/dist/utils/agent-variant.d.ts +63 -0
- package/dist/utils/compat.d.ts +30 -0
- package/dist/utils/env.d.ts +1 -0
- package/dist/utils/index.d.ts +9 -0
- package/dist/utils/internal-initiator.d.ts +6 -0
- package/dist/utils/logger.d.ts +8 -0
- package/dist/utils/polling.d.ts +21 -0
- package/dist/utils/session-manager.d.ts +55 -0
- package/dist/utils/session.d.ts +90 -0
- package/dist/utils/subagent-depth.d.ts +35 -0
- package/dist/utils/system-collapse.d.ts +6 -0
- package/dist/utils/task.d.ts +4 -0
- package/dist/utils/zip-extractor.d.ts +1 -0
- package/index.ts +1 -0
- package/opencode-dux.schema.json +634 -0
- package/package.json +103 -0
- package/src/agents/descriptions.ts +55 -0
- package/src/agents/designer.test.ts +86 -0
- package/src/agents/designer.ts +154 -0
- package/src/agents/display-name.test.ts +186 -0
- package/src/agents/explorer.test.ts +79 -0
- package/src/agents/explorer.ts +144 -0
- package/src/agents/fixer.test.ts +79 -0
- package/src/agents/fixer.ts +145 -0
- package/src/agents/index.test.ts +472 -0
- package/src/agents/index.ts +248 -0
- package/src/agents/interpreter.ts +136 -0
- package/src/agents/librarian.test.ts +80 -0
- package/src/agents/librarian.ts +145 -0
- package/src/agents/oracle.test.ts +89 -0
- package/src/agents/oracle.ts +184 -0
- package/src/agents/orchestrator.test.ts +116 -0
- package/src/agents/orchestrator.ts +574 -0
- package/src/agents/overrides.ts +95 -0
- package/src/agents/prompt-blocks.test.ts +114 -0
- package/src/agents/prompt-blocks.ts +640 -0
- package/src/agents/steward.ts +146 -0
- package/src/cli/config-io.test.ts +536 -0
- package/src/cli/config-io.ts +473 -0
- package/src/cli/config-manager.test.ts +141 -0
- package/src/cli/config-manager.ts +4 -0
- package/src/cli/index.ts +88 -0
- package/src/cli/install.ts +282 -0
- package/src/cli/mcps.test.ts +62 -0
- package/src/cli/mcps.ts +39 -0
- package/src/cli/model-key-normalization.test.ts +21 -0
- package/src/cli/model-key-normalization.ts +60 -0
- package/src/cli/paths.test.ts +167 -0
- package/src/cli/paths.ts +144 -0
- package/src/cli/providers.test.ts +118 -0
- package/src/cli/providers.ts +141 -0
- package/src/cli/skills.test.ts +111 -0
- package/src/cli/skills.ts +103 -0
- package/src/cli/system.test.ts +91 -0
- package/src/cli/system.ts +180 -0
- package/src/cli/types.ts +43 -0
- package/src/config/constants.ts +58 -0
- package/src/config/index.ts +4 -0
- package/src/config/loader.test.ts +1194 -0
- package/src/config/loader.ts +269 -0
- package/src/config/model-resolution.test.ts +176 -0
- package/src/config/runtime-preset.test.ts +61 -0
- package/src/config/runtime-preset.ts +37 -0
- package/src/config/schema.ts +248 -0
- package/src/config/utils.test.ts +41 -0
- package/src/config/utils.ts +23 -0
- package/src/discovery/local/types.ts +85 -0
- package/src/discovery/local.ts +322 -0
- package/src/discovery/mcp-servers.ts +804 -0
- package/src/discovery/skills.ts +959 -0
- package/src/hooks/apply-patch/codec.test.ts +184 -0
- package/src/hooks/apply-patch/codec.ts +352 -0
- package/src/hooks/apply-patch/errors.ts +117 -0
- package/src/hooks/apply-patch/execution-context.ts +432 -0
- package/src/hooks/apply-patch/hook.test.ts +768 -0
- package/src/hooks/apply-patch/index.ts +126 -0
- package/src/hooks/apply-patch/matching.test.ts +215 -0
- package/src/hooks/apply-patch/matching.ts +586 -0
- package/src/hooks/apply-patch/operations.test.ts +1535 -0
- package/src/hooks/apply-patch/operations.ts +3 -0
- package/src/hooks/apply-patch/patch.ts +9 -0
- package/src/hooks/apply-patch/prepared-changes.ts +400 -0
- package/src/hooks/apply-patch/resolution.test.ts +420 -0
- package/src/hooks/apply-patch/resolution.ts +437 -0
- package/src/hooks/apply-patch/rewrite.ts +496 -0
- package/src/hooks/apply-patch/test-helpers.ts +52 -0
- package/src/hooks/apply-patch/types.ts +111 -0
- package/src/hooks/auto-update-checker/cache.test.ts +179 -0
- package/src/hooks/auto-update-checker/cache.ts +188 -0
- package/src/hooks/auto-update-checker/checker.test.ts +159 -0
- package/src/hooks/auto-update-checker/checker.ts +308 -0
- package/src/hooks/auto-update-checker/constants.ts +33 -0
- package/src/hooks/auto-update-checker/index.test.ts +282 -0
- package/src/hooks/auto-update-checker/index.ts +225 -0
- package/src/hooks/auto-update-checker/types.ts +26 -0
- package/src/hooks/chat-headers.test.ts +236 -0
- package/src/hooks/chat-headers.ts +97 -0
- package/src/hooks/context-pressure-reminder/index.test.ts +179 -0
- package/src/hooks/context-pressure-reminder/index.ts +137 -0
- package/src/hooks/delegate-task-retry/guidance.ts +41 -0
- package/src/hooks/delegate-task-retry/hook.ts +23 -0
- package/src/hooks/delegate-task-retry/index.test.ts +38 -0
- package/src/hooks/delegate-task-retry/index.ts +7 -0
- package/src/hooks/delegate-task-retry/patterns.ts +79 -0
- package/src/hooks/filter-available-skills/index.test.ts +297 -0
- package/src/hooks/filter-available-skills/index.ts +160 -0
- package/src/hooks/foreground-fallback/index.test.ts +624 -0
- package/src/hooks/foreground-fallback/index.ts +374 -0
- package/src/hooks/image-hook.ts +6 -0
- package/src/hooks/index.ts +17 -0
- package/src/hooks/json-error-recovery/hook.ts +73 -0
- package/src/hooks/json-error-recovery/index.test.ts +111 -0
- package/src/hooks/json-error-recovery/index.ts +6 -0
- package/src/hooks/phase-reminder/index.test.ts +74 -0
- package/src/hooks/phase-reminder/index.ts +85 -0
- package/src/hooks/post-file-tool-nudge/index.test.ts +94 -0
- package/src/hooks/post-file-tool-nudge/index.ts +63 -0
- package/src/hooks/task-session-manager/index.test.ts +833 -0
- package/src/hooks/task-session-manager/index.ts +434 -0
- package/src/hooks/todo-continuation/index.test.ts +3026 -0
- package/src/hooks/todo-continuation/index.ts +878 -0
- package/src/hooks/todo-continuation/todo-hygiene.test.ts +204 -0
- package/src/hooks/todo-continuation/todo-hygiene.ts +207 -0
- package/src/index.ts +1672 -0
- package/src/mcp/context7.ts +14 -0
- package/src/mcp/grep-app.ts +11 -0
- package/src/mcp/index.test.ts +96 -0
- package/src/mcp/index.ts +66 -0
- package/src/mcp/types.ts +16 -0
- package/src/mcp/websearch.ts +47 -0
- package/src/skills/codemap/README.md +60 -0
- package/src/skills/codemap/SKILL.md +174 -0
- package/src/skills/codemap/scripts/codemap.mjs +483 -0
- package/src/skills/codemap/scripts/codemap.test.ts +129 -0
- package/src/skills/registry.ts +218 -0
- package/src/skills/simplify/README.md +19 -0
- package/src/skills/simplify/SKILL.md +138 -0
- package/src/subscriptions/accounts-store.test.ts +236 -0
- package/src/subscriptions/accounts-store.ts +184 -0
- package/src/subscriptions/index.ts +30 -0
- package/src/subscriptions/neuralwatt-scraper.ts +108 -0
- package/src/subscriptions/opencode-go-scraper.ts +301 -0
- package/src/subscriptions/types.ts +145 -0
- package/src/subscriptions/usage-service.test.ts +202 -0
- package/src/subscriptions/usage-service.ts +651 -0
- package/src/tools/ast-grep/cli.ts +257 -0
- package/src/tools/ast-grep/constants.ts +214 -0
- package/src/tools/ast-grep/downloader.ts +131 -0
- package/src/tools/ast-grep/index.ts +24 -0
- package/src/tools/ast-grep/tools.ts +117 -0
- package/src/tools/ast-grep/types.ts +51 -0
- package/src/tools/ast-grep/utils.ts +126 -0
- package/src/tools/delegate-handoff.test.ts +18 -0
- package/src/tools/delegate.ts +508 -0
- package/src/tools/index.ts +8 -0
- package/src/tools/preset-manager.test.ts +795 -0
- package/src/tools/preset-manager.ts +332 -0
- package/src/tools/smartfetch/binary.ts +58 -0
- package/src/tools/smartfetch/cache.test.ts +34 -0
- package/src/tools/smartfetch/cache.ts +112 -0
- package/src/tools/smartfetch/constants.ts +29 -0
- package/src/tools/smartfetch/index.ts +8 -0
- package/src/tools/smartfetch/network.test.ts +178 -0
- package/src/tools/smartfetch/network.ts +614 -0
- package/src/tools/smartfetch/secondary-model.test.ts +85 -0
- package/src/tools/smartfetch/secondary-model.ts +276 -0
- package/src/tools/smartfetch/tool.test.ts +60 -0
- package/src/tools/smartfetch/tool.ts +832 -0
- package/src/tools/smartfetch/types.ts +135 -0
- package/src/tools/smartfetch/utils.test.ts +24 -0
- package/src/tools/smartfetch/utils.ts +456 -0
- package/src/tui-state.test.ts +867 -0
- package/src/tui-state.ts +1255 -0
- package/src/tui.test.ts +336 -0
- package/src/tui.ts +1539 -0
- package/src/utils/agent-variant.test.ts +244 -0
- package/src/utils/agent-variant.ts +187 -0
- package/src/utils/compat.ts +91 -0
- package/src/utils/env.ts +12 -0
- package/src/utils/index.ts +9 -0
- package/src/utils/internal-initiator.ts +28 -0
- package/src/utils/logger.test.ts +220 -0
- package/src/utils/logger.ts +136 -0
- package/src/utils/polling.test.ts +191 -0
- package/src/utils/polling.ts +67 -0
- package/src/utils/session-manager.test.ts +173 -0
- package/src/utils/session-manager.ts +356 -0
- package/src/utils/session.test.ts +110 -0
- package/src/utils/session.ts +389 -0
- package/src/utils/subagent-depth.test.ts +170 -0
- package/src/utils/subagent-depth.ts +75 -0
- package/src/utils/system-collapse.test.ts +86 -0
- package/src/utils/system-collapse.ts +24 -0
- package/src/utils/task.test.ts +24 -0
- package/src/utils/task.ts +20 -0
- package/src/utils/zip-extractor.ts +102 -0
package/dist/tui.js
ADDED
|
@@ -0,0 +1,1896 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
+
function __accessProp(key) {
|
|
8
|
+
return this[key];
|
|
9
|
+
}
|
|
10
|
+
var __toESMCache_node;
|
|
11
|
+
var __toESMCache_esm;
|
|
12
|
+
var __toESM = (mod, isNodeMode, target) => {
|
|
13
|
+
var canCache = mod != null && typeof mod === "object";
|
|
14
|
+
if (canCache) {
|
|
15
|
+
var cache = isNodeMode ? __toESMCache_node ??= new WeakMap : __toESMCache_esm ??= new WeakMap;
|
|
16
|
+
var cached = cache.get(mod);
|
|
17
|
+
if (cached)
|
|
18
|
+
return cached;
|
|
19
|
+
}
|
|
20
|
+
target = mod != null ? __create(__getProtoOf(mod)) : {};
|
|
21
|
+
const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
|
|
22
|
+
for (let key of __getOwnPropNames(mod))
|
|
23
|
+
if (!__hasOwnProp.call(to, key))
|
|
24
|
+
__defProp(to, key, {
|
|
25
|
+
get: __accessProp.bind(mod, key),
|
|
26
|
+
enumerable: true
|
|
27
|
+
});
|
|
28
|
+
if (canCache)
|
|
29
|
+
cache.set(mod, to);
|
|
30
|
+
return to;
|
|
31
|
+
};
|
|
32
|
+
var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
|
|
33
|
+
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
34
|
+
|
|
35
|
+
// src/tui.ts
|
|
36
|
+
import { createElement, insert, setProp } from "@opentui/solid";
|
|
37
|
+
import { createSignal } from "solid-js";
|
|
38
|
+
|
|
39
|
+
// src/agents/descriptions.ts
|
|
40
|
+
var AGENT_DESCRIPTIONS = {
|
|
41
|
+
explorer: `<agent name="@explorer">
|
|
42
|
+
- Role: codebase search specialist
|
|
43
|
+
- Delegate when: locate files, usages, symbols, tests, config links
|
|
44
|
+
- Do not use when: exact file is already known and must be read in full
|
|
45
|
+
</agent>`,
|
|
46
|
+
librarian: `<agent name="@librarian">
|
|
47
|
+
- Role: external docs and API reference specialist
|
|
48
|
+
- Delegate when: library behavior, version details, official examples, upstream GitHub issues/PRs/releases
|
|
49
|
+
- Do not use when: pure language fundamentals or local code discovery
|
|
50
|
+
</agent>`,
|
|
51
|
+
oracle: `<agent name="@oracle">
|
|
52
|
+
- Role: technical analysis and code review; uses orchestrator \`model\` + \`variant\` matrix
|
|
53
|
+
- Delegate when: debugging, architecture, tradeoffs, risk, any review depth
|
|
54
|
+
- Do not use when: pure local discovery (@explorer) or docs-only (@librarian)
|
|
55
|
+
</agent>`,
|
|
56
|
+
designer: `<agent name="@designer">
|
|
57
|
+
- Role: UI/UX specialist for ALL user-facing UI — new pages, existing component changes, layout updates, styling, visual polish, a11y
|
|
58
|
+
- Delegate when: ANY change to TSX/JSX files, components, pages, layouts, styling, or user-facing visual elements — BEFORE @oracle or @fixer
|
|
59
|
+
- Do not use when: backend-only, non-visual work, or pure logic changes (hooks/utils) that don't affect rendering
|
|
60
|
+
</agent>`,
|
|
61
|
+
fixer: `<agent name="@fixer">
|
|
62
|
+
- Role: implementation specialist
|
|
63
|
+
- Delegate when: edits, tests, scoped commands-after gates in <first_gate> when applicable
|
|
64
|
+
- Do not use when: strategy/conventions/UI design still unresolved-delegate upward first
|
|
65
|
+
</agent>`,
|
|
66
|
+
steward: `<agent name="@steward">
|
|
67
|
+
- Role: rules citation from steward_paths — verbatim excerpts only; does NOT analyze, evaluate, or compare rules
|
|
68
|
+
- Delegate when: repo conventions needed before oracle/fixer (see <first_gate> 1)
|
|
69
|
+
- Do not use when: pure symbol search (@explorer); rules analysis (@oracle).
|
|
70
|
+
</agent>`,
|
|
71
|
+
interpreter: `<agent name="@interpreter">
|
|
72
|
+
- Role: screenshot / attached-image analyst
|
|
73
|
+
- Delegate when: user message has images and task is not redesign-only
|
|
74
|
+
- Do not use when: redesign-only-use @designer; text-only prompts
|
|
75
|
+
</agent>`
|
|
76
|
+
};
|
|
77
|
+
var AGENT_SIDEBAR_DESCRIPTIONS = {
|
|
78
|
+
orchestrator: "Orchestrates",
|
|
79
|
+
explorer: "File Search",
|
|
80
|
+
librarian: "Doc Search",
|
|
81
|
+
oracle: "Architecture",
|
|
82
|
+
designer: "Design",
|
|
83
|
+
fixer: "Implement",
|
|
84
|
+
steward: "Repo rules",
|
|
85
|
+
interpreter: "Vision"
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
// src/tui-state.ts
|
|
89
|
+
import * as fs from "node:fs";
|
|
90
|
+
import * as os from "node:os";
|
|
91
|
+
import * as path from "node:path";
|
|
92
|
+
var sessionTreeStore = {};
|
|
93
|
+
var SESSION_BUNDLE_RETENTION_MS = 7 * 24 * 60 * 60 * 1000;
|
|
94
|
+
function emptyBundle(rootSessionId) {
|
|
95
|
+
return {
|
|
96
|
+
rootSessionId,
|
|
97
|
+
lastActivityAt: Date.now(),
|
|
98
|
+
tree: {},
|
|
99
|
+
orchestrationUsageLastSeen: {}
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
function normalizeProjectDirectory(raw) {
|
|
103
|
+
return path.normalize(path.resolve(raw));
|
|
104
|
+
}
|
|
105
|
+
function mergedSessionTree(snapshot) {
|
|
106
|
+
const out = {};
|
|
107
|
+
for (const bundle of Object.values(snapshot.sessions)) {
|
|
108
|
+
Object.assign(out, bundle.tree);
|
|
109
|
+
}
|
|
110
|
+
return out;
|
|
111
|
+
}
|
|
112
|
+
function deriveSessionContextPct(used, limit) {
|
|
113
|
+
if (!(limit > 0))
|
|
114
|
+
return 0;
|
|
115
|
+
if (!(Number.isFinite(used) && Number.isFinite(limit)))
|
|
116
|
+
return 0;
|
|
117
|
+
const safeUsed = Math.max(0, used);
|
|
118
|
+
return Math.max(0, Math.min(100, safeUsed / limit * 100));
|
|
119
|
+
}
|
|
120
|
+
function coerceSessionUsageEntry(raw) {
|
|
121
|
+
if (!raw || typeof raw !== "object")
|
|
122
|
+
return;
|
|
123
|
+
return {
|
|
124
|
+
contextUsed: typeof raw.contextUsed === "number" ? Math.max(0, raw.contextUsed) : 0,
|
|
125
|
+
contextLimit: typeof raw.contextLimit === "number" ? Math.max(0, raw.contextLimit) : 0,
|
|
126
|
+
contextPct: typeof raw.contextPct === "number" ? Math.max(0, Math.min(100, raw.contextPct)) : 0,
|
|
127
|
+
input: typeof raw.input === "number" ? Math.max(0, raw.input) : 0,
|
|
128
|
+
output: typeof raw.output === "number" ? Math.max(0, raw.output) : 0,
|
|
129
|
+
reasoning: typeof raw.reasoning === "number" ? Math.max(0, raw.reasoning) : 0,
|
|
130
|
+
cacheRead: typeof raw.cacheRead === "number" ? Math.max(0, raw.cacheRead) : 0,
|
|
131
|
+
cacheWrite: typeof raw.cacheWrite === "number" ? Math.max(0, raw.cacheWrite) : 0,
|
|
132
|
+
updatedAt: typeof raw.updatedAt === "number" ? Math.max(0, raw.updatedAt) : 0
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
function mergedSessionUsage(snapshot) {
|
|
136
|
+
const out = {};
|
|
137
|
+
for (const bundle of Object.values(snapshot.sessions)) {
|
|
138
|
+
for (const [sid, node] of Object.entries(bundle.tree)) {
|
|
139
|
+
if (node.usage === undefined)
|
|
140
|
+
continue;
|
|
141
|
+
const usage = coerceSessionUsageEntry(node.usage);
|
|
142
|
+
if (usage)
|
|
143
|
+
out[sid] = usage;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return out;
|
|
147
|
+
}
|
|
148
|
+
function mergedSessionModels(snapshot) {
|
|
149
|
+
const out = {};
|
|
150
|
+
const tree = mergedSessionTree(snapshot);
|
|
151
|
+
for (const [sid, node] of Object.entries(tree)) {
|
|
152
|
+
if (node.model)
|
|
153
|
+
out[sid] = node.model;
|
|
154
|
+
}
|
|
155
|
+
return out;
|
|
156
|
+
}
|
|
157
|
+
function mergedOrchestrationSigmaAccum(snapshot) {
|
|
158
|
+
const out = {};
|
|
159
|
+
for (const [rootId, bundle] of Object.entries(snapshot.sessions)) {
|
|
160
|
+
if (bundle.orchestrationSigmaAccum) {
|
|
161
|
+
out[rootId] = bundle.orchestrationSigmaAccum;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return out;
|
|
165
|
+
}
|
|
166
|
+
function touchBundle(bundle) {
|
|
167
|
+
bundle.lastActivityAt = Date.now();
|
|
168
|
+
}
|
|
169
|
+
function locateBundleForSession(snapshot, sessionID) {
|
|
170
|
+
for (const [rootId, bundle] of Object.entries(snapshot.sessions)) {
|
|
171
|
+
if (bundle.tree[sessionID])
|
|
172
|
+
return { rootId, bundle };
|
|
173
|
+
}
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
function mapOpenCodeStatusToTreeStatus(raw) {
|
|
177
|
+
const t = raw.trim().toLowerCase();
|
|
178
|
+
if (t === "idle")
|
|
179
|
+
return "idle";
|
|
180
|
+
if (t === "retry")
|
|
181
|
+
return "retry";
|
|
182
|
+
if (t === "busy")
|
|
183
|
+
return "busy";
|
|
184
|
+
return "busy";
|
|
185
|
+
}
|
|
186
|
+
function applyOpenCodeSessionStatus(snapshot, sessionID, rawType) {
|
|
187
|
+
const mapped = mapOpenCodeStatusToTreeStatus(rawType);
|
|
188
|
+
const hit = locateBundleForSession(snapshot, sessionID);
|
|
189
|
+
if (hit) {
|
|
190
|
+
hit.bundle.tree[sessionID].status = mapped;
|
|
191
|
+
touchBundle(hit.bundle);
|
|
192
|
+
sessionTreeStore[sessionID] = hit.bundle.tree[sessionID];
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
const store = sessionTreeStore[sessionID];
|
|
196
|
+
if (store)
|
|
197
|
+
store.status = mapped;
|
|
198
|
+
}
|
|
199
|
+
function syncOpenCodeStatusesIntoSessionTree(snapshot, statuses) {
|
|
200
|
+
for (const [sid, row] of Object.entries(statuses)) {
|
|
201
|
+
applyOpenCodeSessionStatus(snapshot, sid, row.type);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
function upwardRootFrom(mergedTree, startSessionId) {
|
|
205
|
+
let cur = startSessionId;
|
|
206
|
+
const visited = new Set;
|
|
207
|
+
while (!visited.has(cur)) {
|
|
208
|
+
visited.add(cur);
|
|
209
|
+
const parent = mergedTree[cur]?.parentId;
|
|
210
|
+
if (!parent)
|
|
211
|
+
break;
|
|
212
|
+
cur = parent;
|
|
213
|
+
}
|
|
214
|
+
return cur;
|
|
215
|
+
}
|
|
216
|
+
function resolveBundleRootForSession(snapshot, sessionID, explicitParentId) {
|
|
217
|
+
const merged = mergedSessionTree(snapshot);
|
|
218
|
+
if (!explicitParentId) {
|
|
219
|
+
if (merged[sessionID])
|
|
220
|
+
return upwardRootFrom(merged, sessionID);
|
|
221
|
+
return sessionID;
|
|
222
|
+
}
|
|
223
|
+
return upwardRootFrom(merged, explicitParentId);
|
|
224
|
+
}
|
|
225
|
+
function ensureBundle(snapshot, rootSessionId) {
|
|
226
|
+
let bundle = snapshot.sessions[rootSessionId];
|
|
227
|
+
if (!bundle) {
|
|
228
|
+
bundle = emptyBundle(rootSessionId);
|
|
229
|
+
snapshot.sessions[rootSessionId] = bundle;
|
|
230
|
+
touchBundle(bundle);
|
|
231
|
+
}
|
|
232
|
+
return bundle;
|
|
233
|
+
}
|
|
234
|
+
function getOrCreateTreeNode(bundle, sessionID) {
|
|
235
|
+
const merged = sessionTreeStore[sessionID] ?? bundle.tree[sessionID] ?? {
|
|
236
|
+
title: "",
|
|
237
|
+
agent: "",
|
|
238
|
+
model: "",
|
|
239
|
+
childIds: [],
|
|
240
|
+
status: "busy",
|
|
241
|
+
createdAt: Date.now()
|
|
242
|
+
};
|
|
243
|
+
bundle.tree[sessionID] = merged;
|
|
244
|
+
sessionTreeStore[sessionID] = merged;
|
|
245
|
+
return merged;
|
|
246
|
+
}
|
|
247
|
+
function deleteBundleCascade(snapshot, rootSessionId) {
|
|
248
|
+
const bundle = snapshot.sessions[rootSessionId];
|
|
249
|
+
if (!bundle)
|
|
250
|
+
return new Set;
|
|
251
|
+
const removedIds = new Set(Object.keys(bundle.tree));
|
|
252
|
+
delete snapshot.sessions[rootSessionId];
|
|
253
|
+
return removedIds;
|
|
254
|
+
}
|
|
255
|
+
function pruneSessionSidDataInBundle(bundle, sid) {
|
|
256
|
+
const node = bundle.tree[sid];
|
|
257
|
+
if (node) {
|
|
258
|
+
const needsFlashStart = node.status !== "idle" || node.finishedAt === undefined;
|
|
259
|
+
node.status = "idle";
|
|
260
|
+
if (needsFlashStart) {
|
|
261
|
+
node.finishedAt = Date.now();
|
|
262
|
+
}
|
|
263
|
+
delete node.usage;
|
|
264
|
+
}
|
|
265
|
+
delete bundle.orchestrationUsageLastSeen[sid];
|
|
266
|
+
}
|
|
267
|
+
function normalizedBundleProjectForSession(snapshot, sessionID) {
|
|
268
|
+
const hit = locateBundleForSession(snapshot, sessionID);
|
|
269
|
+
if (!hit?.bundle.projectPath)
|
|
270
|
+
return;
|
|
271
|
+
return normalizeProjectDirectory(hit.bundle.projectPath);
|
|
272
|
+
}
|
|
273
|
+
function expandMissingSessionCascade(mergedTree, seeds) {
|
|
274
|
+
const ids = new Set(seeds);
|
|
275
|
+
let added = true;
|
|
276
|
+
while (added) {
|
|
277
|
+
added = false;
|
|
278
|
+
for (const [sid, node] of Object.entries(mergedTree)) {
|
|
279
|
+
if (ids.has(sid))
|
|
280
|
+
continue;
|
|
281
|
+
const parentId = node.parentId;
|
|
282
|
+
if (parentId && ids.has(parentId)) {
|
|
283
|
+
ids.add(sid);
|
|
284
|
+
added = true;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
return ids;
|
|
289
|
+
}
|
|
290
|
+
function isStrictDescendantInMergedTree(mergedTree, ancestorId, descendantCandidate) {
|
|
291
|
+
if (ancestorId === descendantCandidate)
|
|
292
|
+
return false;
|
|
293
|
+
let cur = descendantCandidate;
|
|
294
|
+
const visited = new Set;
|
|
295
|
+
while (cur && !visited.has(cur)) {
|
|
296
|
+
visited.add(cur);
|
|
297
|
+
if (cur === ancestorId)
|
|
298
|
+
return true;
|
|
299
|
+
cur = mergedTree[cur]?.parentId;
|
|
300
|
+
}
|
|
301
|
+
return false;
|
|
302
|
+
}
|
|
303
|
+
function softPruneTargetHasPollDescendant(mergedTree, targetSid, opencodeIds) {
|
|
304
|
+
for (const pollId of opencodeIds) {
|
|
305
|
+
if (isStrictDescendantInMergedTree(mergedTree, targetSid, pollId)) {
|
|
306
|
+
return true;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
return false;
|
|
310
|
+
}
|
|
311
|
+
function pruneStaleTuiSessionBundles(snapshot, input) {
|
|
312
|
+
const strippedFromFile = new Set;
|
|
313
|
+
for (const rootId of Object.keys(snapshot.sessions)) {
|
|
314
|
+
const bundle = snapshot.sessions[rootId];
|
|
315
|
+
if (!bundle)
|
|
316
|
+
continue;
|
|
317
|
+
if (bundle.lastActivityAt > 0 && input.now - bundle.lastActivityAt >= SESSION_BUNDLE_RETENTION_MS) {
|
|
318
|
+
for (const id of deleteBundleCascade(snapshot, rootId)) {
|
|
319
|
+
strippedFromFile.add(id);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
const projectMatched = normalizeProjectDirectory(input.currentProjectDir);
|
|
324
|
+
if (input.opencodeIds.size > 0) {
|
|
325
|
+
for (const rootId of [...Object.keys(snapshot.sessions)]) {
|
|
326
|
+
const bundle = snapshot.sessions[rootId];
|
|
327
|
+
if (!bundle?.projectPath)
|
|
328
|
+
continue;
|
|
329
|
+
if (normalizeProjectDirectory(bundle.projectPath) !== projectMatched) {
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
332
|
+
const treeIds = Object.keys(bundle.tree);
|
|
333
|
+
if (treeIds.length === 0)
|
|
334
|
+
continue;
|
|
335
|
+
if (treeIds.every((id) => !input.opencodeIds.has(id))) {
|
|
336
|
+
for (const id of deleteBundleCascade(snapshot, rootId)) {
|
|
337
|
+
strippedFromFile.add(id);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
const merged = mergedSessionTree(snapshot);
|
|
343
|
+
const missingSeeds = input.opencodeIds.size > 0 ? Object.keys(merged).filter((id) => !input.opencodeIds.has(id)) : [];
|
|
344
|
+
const expandedMissing = expandMissingSessionCascade(merged, missingSeeds);
|
|
345
|
+
for (const sid of expandedMissing) {
|
|
346
|
+
if (input.opencodeIds.has(sid))
|
|
347
|
+
continue;
|
|
348
|
+
if (softPruneTargetHasPollDescendant(merged, sid, input.opencodeIds)) {
|
|
349
|
+
continue;
|
|
350
|
+
}
|
|
351
|
+
const projected = normalizedBundleProjectForSession(snapshot, sid);
|
|
352
|
+
if (projected === undefined || projected !== projectMatched)
|
|
353
|
+
continue;
|
|
354
|
+
const located = locateBundleForSession(snapshot, sid);
|
|
355
|
+
if (!located)
|
|
356
|
+
continue;
|
|
357
|
+
const { bundle, rootId } = located;
|
|
358
|
+
pruneSessionSidDataInBundle(bundle, sid);
|
|
359
|
+
strippedFromFile.add(sid);
|
|
360
|
+
if (located.bundle.tree[sid]?.agent === "orchestrator" && sid === rootId) {
|
|
361
|
+
delete bundle.orchestrationSigmaAccum;
|
|
362
|
+
}
|
|
363
|
+
touchBundle(bundle);
|
|
364
|
+
}
|
|
365
|
+
return strippedFromFile;
|
|
366
|
+
}
|
|
367
|
+
var STATE_DIR = "opencode-dux";
|
|
368
|
+
var STATE_FILE = "tui-state.json";
|
|
369
|
+
function dataDir() {
|
|
370
|
+
return process.env.XDG_DATA_HOME ?? path.join(os.homedir(), ".local", "share");
|
|
371
|
+
}
|
|
372
|
+
function getTuiStatePath() {
|
|
373
|
+
return path.join(dataDir(), "opencode", "storage", STATE_DIR, STATE_FILE);
|
|
374
|
+
}
|
|
375
|
+
function emptySnapshot() {
|
|
376
|
+
return {
|
|
377
|
+
version: 6,
|
|
378
|
+
updatedAt: Date.now(),
|
|
379
|
+
sessions: {},
|
|
380
|
+
subscriptionUsage: {},
|
|
381
|
+
activeSubscriptionByProvider: {}
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
function normalizeSubscriptionUsage(usage) {
|
|
385
|
+
return usage;
|
|
386
|
+
}
|
|
387
|
+
function normalizeSigmaAccum(value) {
|
|
388
|
+
if (!value)
|
|
389
|
+
return;
|
|
390
|
+
return {
|
|
391
|
+
contextUsed: typeof value.contextUsed === "number" ? Math.max(0, value.contextUsed) : 0,
|
|
392
|
+
input: typeof value.input === "number" ? Math.max(0, value.input) : 0,
|
|
393
|
+
output: typeof value.output === "number" ? Math.max(0, value.output) : 0,
|
|
394
|
+
cacheRead: typeof value.cacheRead === "number" ? Math.max(0, value.cacheRead) : 0,
|
|
395
|
+
cacheWrite: typeof value.cacheWrite === "number" ? Math.max(0, value.cacheWrite) : 0
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
function normalizeUsageLastSeen(value) {
|
|
399
|
+
const result = {};
|
|
400
|
+
for (const [sessionID, entry] of Object.entries(value)) {
|
|
401
|
+
if (!entry)
|
|
402
|
+
continue;
|
|
403
|
+
result[sessionID] = {
|
|
404
|
+
contextUsed: typeof entry.contextUsed === "number" ? Math.max(0, entry.contextUsed) : 0,
|
|
405
|
+
input: typeof entry.input === "number" ? Math.max(0, entry.input) : 0,
|
|
406
|
+
output: typeof entry.output === "number" ? Math.max(0, entry.output) : 0,
|
|
407
|
+
cacheRead: typeof entry.cacheRead === "number" ? Math.max(0, entry.cacheRead) : 0,
|
|
408
|
+
cacheWrite: typeof entry.cacheWrite === "number" ? Math.max(0, entry.cacheWrite) : 0
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
return result;
|
|
412
|
+
}
|
|
413
|
+
function hydrateTreeUsages(tree) {
|
|
414
|
+
for (const node of Object.values(tree)) {
|
|
415
|
+
if (node.usage === undefined || node.usage === null)
|
|
416
|
+
continue;
|
|
417
|
+
const u = coerceSessionUsageEntry(node.usage);
|
|
418
|
+
if (u)
|
|
419
|
+
node.usage = u;
|
|
420
|
+
else
|
|
421
|
+
delete node.usage;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
function parseSessionBundles(raw) {
|
|
425
|
+
const out = {};
|
|
426
|
+
if (!raw || typeof raw !== "object")
|
|
427
|
+
return out;
|
|
428
|
+
const entries = Object.entries(raw);
|
|
429
|
+
for (const [rootId, value] of entries) {
|
|
430
|
+
if (!value || typeof value !== "object")
|
|
431
|
+
continue;
|
|
432
|
+
const v = value;
|
|
433
|
+
const tree = v.tree && typeof v.tree === "object" ? v.tree : {};
|
|
434
|
+
hydrateTreeUsages(tree);
|
|
435
|
+
const lastActivityAt = typeof v.lastActivityAt === "number" ? v.lastActivityAt : Date.now();
|
|
436
|
+
const projectPath = typeof v.projectPath === "string" && v.projectPath.length > 0 ? normalizeProjectDirectory(v.projectPath) : undefined;
|
|
437
|
+
const bundle = {
|
|
438
|
+
rootSessionId: typeof v.rootSessionId === "string" && v.rootSessionId.length > 0 ? v.rootSessionId : rootId,
|
|
439
|
+
lastActivityAt,
|
|
440
|
+
projectPath,
|
|
441
|
+
tree,
|
|
442
|
+
orchestrationSigmaAccum: normalizeSigmaAccum(v.orchestrationSigmaAccum && typeof v.orchestrationSigmaAccum === "object" ? v.orchestrationSigmaAccum : undefined),
|
|
443
|
+
orchestrationUsageLastSeen: normalizeUsageLastSeen(typeof v.orchestrationUsageLastSeen === "object" && v.orchestrationUsageLastSeen ? v.orchestrationUsageLastSeen : {})
|
|
444
|
+
};
|
|
445
|
+
out[rootId] = bundle;
|
|
446
|
+
}
|
|
447
|
+
return out;
|
|
448
|
+
}
|
|
449
|
+
function parseSnapshot(value) {
|
|
450
|
+
try {
|
|
451
|
+
const parsed = JSON.parse(value);
|
|
452
|
+
if (parsed?.version !== 6)
|
|
453
|
+
return null;
|
|
454
|
+
const activeSubscriptionByProvider = {};
|
|
455
|
+
if (parsed.activeSubscriptionByProvider) {
|
|
456
|
+
for (const provider of ["opencode-go", "neuralwatt"]) {
|
|
457
|
+
const name = parsed.activeSubscriptionByProvider[provider];
|
|
458
|
+
if (typeof name === "string" && name.length > 0) {
|
|
459
|
+
activeSubscriptionByProvider[provider] = name;
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
return {
|
|
464
|
+
version: 6,
|
|
465
|
+
updatedAt: typeof parsed.updatedAt === "number" ? parsed.updatedAt : Date.now(),
|
|
466
|
+
sessions: parseSessionBundles(parsed.sessions ?? {}),
|
|
467
|
+
subscriptionUsage: normalizeSubscriptionUsage(typeof parsed.subscriptionUsage === "object" && parsed.subscriptionUsage ? parsed.subscriptionUsage : {}),
|
|
468
|
+
activeSubscriptionByProvider
|
|
469
|
+
};
|
|
470
|
+
} catch {
|
|
471
|
+
return null;
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
function tryReadSnapshot() {
|
|
475
|
+
const filePath = getTuiStatePath();
|
|
476
|
+
try {
|
|
477
|
+
const parsed = parseSnapshot(fs.readFileSync(filePath, "utf8"));
|
|
478
|
+
if (parsed) {
|
|
479
|
+
return { snapshot: parsed, okForMutation: true };
|
|
480
|
+
}
|
|
481
|
+
return { snapshot: emptySnapshot(), okForMutation: false };
|
|
482
|
+
} catch (error) {
|
|
483
|
+
if (error.code === "ENOENT") {
|
|
484
|
+
return { snapshot: emptySnapshot(), okForMutation: true };
|
|
485
|
+
}
|
|
486
|
+
return { snapshot: emptySnapshot(), okForMutation: false };
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
function readTuiSnapshot() {
|
|
490
|
+
return tryReadSnapshot().snapshot;
|
|
491
|
+
}
|
|
492
|
+
async function readTuiSnapshotAsync() {
|
|
493
|
+
try {
|
|
494
|
+
const parsed = parseSnapshot(await fs.promises.readFile(getTuiStatePath(), "utf8"));
|
|
495
|
+
return parsed ?? emptySnapshot();
|
|
496
|
+
} catch {
|
|
497
|
+
return emptySnapshot();
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
function writeTuiSnapshot(snapshot) {
|
|
501
|
+
try {
|
|
502
|
+
const filePath = getTuiStatePath();
|
|
503
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
504
|
+
fs.writeFileSync(filePath, `${JSON.stringify(snapshot)}
|
|
505
|
+
`);
|
|
506
|
+
} catch {}
|
|
507
|
+
}
|
|
508
|
+
var isDrainingSnapshot = false;
|
|
509
|
+
var snapshotMutatorQueue = [];
|
|
510
|
+
function updateSnapshot(mutator) {
|
|
511
|
+
snapshotMutatorQueue.push(mutator);
|
|
512
|
+
if (isDrainingSnapshot) {
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
isDrainingSnapshot = true;
|
|
516
|
+
try {
|
|
517
|
+
while (snapshotMutatorQueue.length > 0) {
|
|
518
|
+
try {
|
|
519
|
+
const { snapshot, okForMutation } = tryReadSnapshot();
|
|
520
|
+
if (!okForMutation) {
|
|
521
|
+
snapshotMutatorQueue.length = 0;
|
|
522
|
+
break;
|
|
523
|
+
}
|
|
524
|
+
while (snapshotMutatorQueue.length > 0) {
|
|
525
|
+
const m = snapshotMutatorQueue.shift();
|
|
526
|
+
if (m === undefined) {
|
|
527
|
+
break;
|
|
528
|
+
}
|
|
529
|
+
m(snapshot);
|
|
530
|
+
}
|
|
531
|
+
snapshot.updatedAt = Date.now();
|
|
532
|
+
writeTuiSnapshot(snapshot);
|
|
533
|
+
} catch {
|
|
534
|
+
snapshotMutatorQueue.length = 0;
|
|
535
|
+
break;
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
} finally {
|
|
539
|
+
isDrainingSnapshot = false;
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
function applyRecordSessionUsageToSnapshot(snapshot, input) {
|
|
543
|
+
let bundle;
|
|
544
|
+
const located = locateBundleForSession(snapshot, input.sessionID);
|
|
545
|
+
if (located)
|
|
546
|
+
bundle = located.bundle;
|
|
547
|
+
else {
|
|
548
|
+
const rootFallback = resolveBundleRootForSession(snapshot, input.sessionID);
|
|
549
|
+
bundle = ensureBundle(snapshot, rootFallback);
|
|
550
|
+
}
|
|
551
|
+
const node = getOrCreateTreeNode(bundle, input.sessionID);
|
|
552
|
+
const prev = coerceSessionUsageEntry(node.usage);
|
|
553
|
+
const nextContextUsed = input.contextUsed !== undefined ? Math.max(0, input.contextUsed) : prev?.contextUsed ?? 0;
|
|
554
|
+
const nextContextLimit = input.contextLimit != null && input.contextLimit > 0 ? input.contextLimit : prev?.contextLimit ?? 0;
|
|
555
|
+
const next = {
|
|
556
|
+
contextUsed: nextContextUsed,
|
|
557
|
+
contextLimit: nextContextLimit,
|
|
558
|
+
contextPct: deriveSessionContextPct(nextContextUsed, nextContextLimit),
|
|
559
|
+
input: input.input !== undefined ? Math.max(prev?.input ?? 0, input.input) : prev?.input ?? 0,
|
|
560
|
+
output: input.output !== undefined ? Math.max(prev?.output ?? 0, input.output) : prev?.output ?? 0,
|
|
561
|
+
reasoning: input.reasoning !== undefined ? Math.max(prev?.reasoning ?? 0, input.reasoning) : prev?.reasoning ?? 0,
|
|
562
|
+
cacheRead: input.cacheRead !== undefined ? Math.max(prev?.cacheRead ?? 0, input.cacheRead) : prev?.cacheRead ?? 0,
|
|
563
|
+
cacheWrite: input.cacheWrite !== undefined ? Math.max(prev?.cacheWrite ?? 0, input.cacheWrite) : prev?.cacheWrite ?? 0,
|
|
564
|
+
updatedAt: Date.now()
|
|
565
|
+
};
|
|
566
|
+
node.usage = next;
|
|
567
|
+
touchBundle(bundle);
|
|
568
|
+
const rootSessionID = resolveOrchestrationRootSessionID(snapshot, input.sessionID);
|
|
569
|
+
if (!rootSessionID)
|
|
570
|
+
return;
|
|
571
|
+
const orchBundle = locateBundleForSession(snapshot, rootSessionID);
|
|
572
|
+
if (!orchBundle)
|
|
573
|
+
return;
|
|
574
|
+
const previousSeen = orchBundle.bundle.orchestrationUsageLastSeen[input.sessionID] ?? {
|
|
575
|
+
contextUsed: 0,
|
|
576
|
+
input: 0,
|
|
577
|
+
output: 0,
|
|
578
|
+
cacheRead: 0,
|
|
579
|
+
cacheWrite: 0
|
|
580
|
+
};
|
|
581
|
+
const nextSeen = {
|
|
582
|
+
contextUsed: next.contextUsed,
|
|
583
|
+
input: next.input,
|
|
584
|
+
output: next.output,
|
|
585
|
+
cacheRead: next.cacheRead,
|
|
586
|
+
cacheWrite: next.cacheWrite
|
|
587
|
+
};
|
|
588
|
+
const deltaContextUsed = Math.max(0, nextSeen.contextUsed - previousSeen.contextUsed);
|
|
589
|
+
const deltaInput = Math.max(0, nextSeen.input - previousSeen.input);
|
|
590
|
+
const deltaOutput = Math.max(0, nextSeen.output - previousSeen.output);
|
|
591
|
+
const deltaCacheRead = Math.max(0, nextSeen.cacheRead - previousSeen.cacheRead);
|
|
592
|
+
const deltaCacheWrite = Math.max(0, nextSeen.cacheWrite - previousSeen.cacheWrite);
|
|
593
|
+
const prevAccum = orchBundle.bundle.orchestrationSigmaAccum ?? {
|
|
594
|
+
contextUsed: 0,
|
|
595
|
+
input: 0,
|
|
596
|
+
output: 0,
|
|
597
|
+
cacheRead: 0,
|
|
598
|
+
cacheWrite: 0
|
|
599
|
+
};
|
|
600
|
+
orchBundle.bundle.orchestrationSigmaAccum = {
|
|
601
|
+
contextUsed: prevAccum.contextUsed + deltaContextUsed,
|
|
602
|
+
input: prevAccum.input + deltaInput,
|
|
603
|
+
output: prevAccum.output + deltaOutput,
|
|
604
|
+
cacheRead: prevAccum.cacheRead + deltaCacheRead,
|
|
605
|
+
cacheWrite: prevAccum.cacheWrite + deltaCacheWrite
|
|
606
|
+
};
|
|
607
|
+
orchBundle.bundle.orchestrationUsageLastSeen[input.sessionID] = nextSeen;
|
|
608
|
+
touchBundle(orchBundle.bundle);
|
|
609
|
+
}
|
|
610
|
+
function recordSessionUsagesBatch(inputs) {
|
|
611
|
+
if (inputs.length === 0)
|
|
612
|
+
return;
|
|
613
|
+
updateSnapshot((snapshot) => {
|
|
614
|
+
for (const input of inputs) {
|
|
615
|
+
applyRecordSessionUsageToSnapshot(snapshot, input);
|
|
616
|
+
}
|
|
617
|
+
});
|
|
618
|
+
}
|
|
619
|
+
function recordDelegatedSubagentSession(input) {
|
|
620
|
+
updateSnapshot((snapshot) => {
|
|
621
|
+
const rootId = resolveBundleRootForSession(snapshot, input.sessionID, input.parentSessionId);
|
|
622
|
+
const bundle = ensureBundle(snapshot, rootId);
|
|
623
|
+
const existing = sessionTreeStore[input.sessionID] ?? bundle.tree[input.sessionID] ?? {
|
|
624
|
+
title: "",
|
|
625
|
+
agent: "",
|
|
626
|
+
model: "",
|
|
627
|
+
childIds: [],
|
|
628
|
+
status: "busy",
|
|
629
|
+
createdAt: Date.now()
|
|
630
|
+
};
|
|
631
|
+
const node = {
|
|
632
|
+
...existing,
|
|
633
|
+
title: existing.title,
|
|
634
|
+
agent: input.agent || existing.agent,
|
|
635
|
+
model: existing.model,
|
|
636
|
+
variant: input.variant !== undefined ? input.variant : existing.variant,
|
|
637
|
+
parentId: input.parentSessionId,
|
|
638
|
+
mode: input.mode !== undefined ? input.mode : existing.mode,
|
|
639
|
+
status: existing.status,
|
|
640
|
+
createdAt: existing.createdAt
|
|
641
|
+
};
|
|
642
|
+
bundle.tree[input.sessionID] = node;
|
|
643
|
+
sessionTreeStore[input.sessionID] = node;
|
|
644
|
+
touchBundle(bundle);
|
|
645
|
+
for (const b of Object.values(snapshot.sessions)) {
|
|
646
|
+
const parent = b.tree[input.parentSessionId];
|
|
647
|
+
if (!parent)
|
|
648
|
+
continue;
|
|
649
|
+
if (!parent.childIds.includes(input.sessionID)) {
|
|
650
|
+
parent.childIds.push(input.sessionID);
|
|
651
|
+
}
|
|
652
|
+
b.lastActivityAt = Date.now();
|
|
653
|
+
}
|
|
654
|
+
const storeParent = sessionTreeStore[input.parentSessionId];
|
|
655
|
+
if (storeParent && !storeParent.childIds.includes(input.sessionID)) {
|
|
656
|
+
storeParent.childIds.push(input.sessionID);
|
|
657
|
+
}
|
|
658
|
+
});
|
|
659
|
+
}
|
|
660
|
+
function recordChildSessionSnapshot(input) {
|
|
661
|
+
updateSnapshot((snapshot) => {
|
|
662
|
+
const rootId = resolveBundleRootForSession(snapshot, input.sessionID, input.parentSessionId);
|
|
663
|
+
const bundle = ensureBundle(snapshot, rootId);
|
|
664
|
+
const existing = sessionTreeStore[input.sessionID] ?? bundle.tree[input.sessionID] ?? {
|
|
665
|
+
title: "",
|
|
666
|
+
agent: "",
|
|
667
|
+
model: "",
|
|
668
|
+
childIds: [],
|
|
669
|
+
status: "busy",
|
|
670
|
+
createdAt: Date.now()
|
|
671
|
+
};
|
|
672
|
+
const node = {
|
|
673
|
+
...existing,
|
|
674
|
+
title: input.title ?? existing.title,
|
|
675
|
+
agent: existing.agent,
|
|
676
|
+
model: existing.model,
|
|
677
|
+
variant: existing.variant,
|
|
678
|
+
parentId: input.parentSessionId !== undefined ? input.parentSessionId : existing.parentId,
|
|
679
|
+
mode: existing.mode,
|
|
680
|
+
status: existing.status,
|
|
681
|
+
createdAt: existing.createdAt
|
|
682
|
+
};
|
|
683
|
+
bundle.tree[input.sessionID] = node;
|
|
684
|
+
sessionTreeStore[input.sessionID] = node;
|
|
685
|
+
touchBundle(bundle);
|
|
686
|
+
if (input.parentSessionId) {
|
|
687
|
+
for (const b of Object.values(snapshot.sessions)) {
|
|
688
|
+
const parent = b.tree[input.parentSessionId];
|
|
689
|
+
if (!parent)
|
|
690
|
+
continue;
|
|
691
|
+
if (!parent.childIds.includes(input.sessionID)) {
|
|
692
|
+
parent.childIds.push(input.sessionID);
|
|
693
|
+
}
|
|
694
|
+
b.lastActivityAt = Date.now();
|
|
695
|
+
}
|
|
696
|
+
const storeParent = sessionTreeStore[input.parentSessionId];
|
|
697
|
+
if (storeParent && !storeParent.childIds.includes(input.sessionID)) {
|
|
698
|
+
storeParent.childIds.push(input.sessionID);
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
if (input.projectPath !== undefined && input.projectPath.length > 0) {
|
|
702
|
+
const normalized = normalizeProjectDirectory(input.projectPath);
|
|
703
|
+
const rootForProject = resolveBundleRootForSession(snapshot, input.sessionID);
|
|
704
|
+
const projectBundle = ensureBundle(snapshot, rootForProject);
|
|
705
|
+
projectBundle.projectPath = normalized;
|
|
706
|
+
touchBundle(projectBundle);
|
|
707
|
+
}
|
|
708
|
+
});
|
|
709
|
+
}
|
|
710
|
+
function patchSessionTreeStatusFromOpenCode(sessionID, rawType) {
|
|
711
|
+
updateSnapshot((snapshot) => {
|
|
712
|
+
applyOpenCodeSessionStatus(snapshot, sessionID, rawType);
|
|
713
|
+
});
|
|
714
|
+
}
|
|
715
|
+
function recordSessionEnd(sessionID) {
|
|
716
|
+
updateSnapshot((snapshot) => {
|
|
717
|
+
const located = locateBundleForSession(snapshot, sessionID);
|
|
718
|
+
const node = located?.bundle.tree[sessionID] ?? sessionTreeStore[sessionID];
|
|
719
|
+
if (node)
|
|
720
|
+
delete node.usage;
|
|
721
|
+
if (located)
|
|
722
|
+
touchBundle(located.bundle);
|
|
723
|
+
});
|
|
724
|
+
}
|
|
725
|
+
function recordSessionModel(input) {
|
|
726
|
+
updateSnapshot((snapshot) => {
|
|
727
|
+
const rootId = resolveBundleRootForSession(snapshot, input.sessionID);
|
|
728
|
+
const bundle = ensureBundle(snapshot, rootId);
|
|
729
|
+
const node = getOrCreateTreeNode(bundle, input.sessionID);
|
|
730
|
+
node.model = input.model;
|
|
731
|
+
touchBundle(bundle);
|
|
732
|
+
});
|
|
733
|
+
}
|
|
734
|
+
function recordSessionVariant(input) {
|
|
735
|
+
updateSnapshot((snapshot) => {
|
|
736
|
+
const rootId = resolveBundleRootForSession(snapshot, input.sessionID);
|
|
737
|
+
const bundle = ensureBundle(snapshot, rootId);
|
|
738
|
+
const node = getOrCreateTreeNode(bundle, input.sessionID);
|
|
739
|
+
node.variant = input.variant;
|
|
740
|
+
touchBundle(bundle);
|
|
741
|
+
});
|
|
742
|
+
}
|
|
743
|
+
function recordSessionNode(input) {
|
|
744
|
+
updateSnapshot((snapshot) => {
|
|
745
|
+
const rootId = resolveBundleRootForSession(snapshot, input.sessionID, input.parentId);
|
|
746
|
+
const bundle = ensureBundle(snapshot, rootId);
|
|
747
|
+
const existing = sessionTreeStore[input.sessionID] ?? bundle.tree[input.sessionID] ?? {
|
|
748
|
+
title: "",
|
|
749
|
+
agent: "",
|
|
750
|
+
model: "",
|
|
751
|
+
childIds: [],
|
|
752
|
+
status: "busy",
|
|
753
|
+
createdAt: Date.now()
|
|
754
|
+
};
|
|
755
|
+
const node = {
|
|
756
|
+
...existing,
|
|
757
|
+
title: input.title !== undefined ? input.title : existing.title,
|
|
758
|
+
agent: input.agent || existing.agent,
|
|
759
|
+
model: input.model ?? existing.model,
|
|
760
|
+
variant: input.variant !== undefined ? input.variant : existing.variant,
|
|
761
|
+
parentId: input.parentId !== undefined ? input.parentId : existing.parentId,
|
|
762
|
+
mode: input.mode !== undefined ? input.mode : existing.mode,
|
|
763
|
+
status: input.status ?? existing.status,
|
|
764
|
+
createdAt: existing.createdAt
|
|
765
|
+
};
|
|
766
|
+
bundle.tree[input.sessionID] = node;
|
|
767
|
+
sessionTreeStore[input.sessionID] = node;
|
|
768
|
+
touchBundle(bundle);
|
|
769
|
+
});
|
|
770
|
+
}
|
|
771
|
+
function recordSessionTitle(input) {
|
|
772
|
+
const trimmed = input.title.trim();
|
|
773
|
+
if (!trimmed)
|
|
774
|
+
return;
|
|
775
|
+
updateSnapshot((snapshot) => {
|
|
776
|
+
const hit = locateBundleForSession(snapshot, input.sessionID);
|
|
777
|
+
if (hit) {
|
|
778
|
+
const node2 = hit.bundle.tree[input.sessionID];
|
|
779
|
+
if (node2) {
|
|
780
|
+
node2.title = trimmed;
|
|
781
|
+
sessionTreeStore[input.sessionID] = node2;
|
|
782
|
+
touchBundle(hit.bundle);
|
|
783
|
+
}
|
|
784
|
+
return;
|
|
785
|
+
}
|
|
786
|
+
const rootId = resolveBundleRootForSession(snapshot, input.sessionID);
|
|
787
|
+
const bundle = ensureBundle(snapshot, rootId);
|
|
788
|
+
const node = getOrCreateTreeNode(bundle, input.sessionID);
|
|
789
|
+
node.title = trimmed;
|
|
790
|
+
touchBundle(bundle);
|
|
791
|
+
});
|
|
792
|
+
}
|
|
793
|
+
function recordSessionDone(sessionID) {
|
|
794
|
+
updateSnapshot((snapshot) => {
|
|
795
|
+
const hit = locateBundleForSession(snapshot, sessionID);
|
|
796
|
+
if (hit) {
|
|
797
|
+
const node = hit.bundle.tree[sessionID];
|
|
798
|
+
if (node) {
|
|
799
|
+
node.status = "idle";
|
|
800
|
+
node.finishedAt = Date.now();
|
|
801
|
+
}
|
|
802
|
+
touchBundle(hit.bundle);
|
|
803
|
+
}
|
|
804
|
+
const storeNode = sessionTreeStore[sessionID];
|
|
805
|
+
if (storeNode) {
|
|
806
|
+
storeNode.status = "idle";
|
|
807
|
+
storeNode.finishedAt = Date.now();
|
|
808
|
+
}
|
|
809
|
+
});
|
|
810
|
+
}
|
|
811
|
+
function resolveOrchestrationRootSessionID(snapshot, sessionID) {
|
|
812
|
+
const merged = mergedSessionTree(snapshot);
|
|
813
|
+
let currentID = sessionID;
|
|
814
|
+
const visited = new Set;
|
|
815
|
+
while (currentID && !visited.has(currentID)) {
|
|
816
|
+
visited.add(currentID);
|
|
817
|
+
const treeNode = merged[currentID];
|
|
818
|
+
if (!treeNode)
|
|
819
|
+
return null;
|
|
820
|
+
if (treeNode.agent === "orchestrator")
|
|
821
|
+
return currentID;
|
|
822
|
+
currentID = treeNode.parentId;
|
|
823
|
+
}
|
|
824
|
+
return null;
|
|
825
|
+
}
|
|
826
|
+
function recordSessionUsage(input) {
|
|
827
|
+
updateSnapshot((snapshot) => {
|
|
828
|
+
applyRecordSessionUsageToSnapshot(snapshot, input);
|
|
829
|
+
});
|
|
830
|
+
}
|
|
831
|
+
function subscriptionUsageKey(provider, accountName) {
|
|
832
|
+
return `${provider}\x00${accountName}`;
|
|
833
|
+
}
|
|
834
|
+
function recordSubscriptionUsage(usage) {
|
|
835
|
+
updateSnapshot((snapshot) => {
|
|
836
|
+
snapshot.subscriptionUsage = {};
|
|
837
|
+
for (const entry of usage) {
|
|
838
|
+
if (entry.accountName) {
|
|
839
|
+
snapshot.subscriptionUsage[subscriptionUsageKey(entry.provider, entry.accountName)] = entry;
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
});
|
|
843
|
+
}
|
|
844
|
+
function removeSubscriptionUsageEntry(provider, name) {
|
|
845
|
+
updateSnapshot((snapshot) => {
|
|
846
|
+
delete snapshot.subscriptionUsage[subscriptionUsageKey(provider, name)];
|
|
847
|
+
});
|
|
848
|
+
}
|
|
849
|
+
function recordSessionProject(input) {
|
|
850
|
+
const normalized = normalizeProjectDirectory(input.projectPath);
|
|
851
|
+
updateSnapshot((snapshot) => {
|
|
852
|
+
const rootId = resolveBundleRootForSession(snapshot, input.sessionID);
|
|
853
|
+
const bundle = ensureBundle(snapshot, rootId);
|
|
854
|
+
bundle.projectPath = normalized;
|
|
855
|
+
touchBundle(bundle);
|
|
856
|
+
});
|
|
857
|
+
}
|
|
858
|
+
function deleteSessionEntries(sessionID) {
|
|
859
|
+
updateSnapshot((snapshot) => {
|
|
860
|
+
const located = locateBundleForSession(snapshot, sessionID);
|
|
861
|
+
if (!located)
|
|
862
|
+
return;
|
|
863
|
+
const { bundle, rootId } = located;
|
|
864
|
+
delete bundle.orchestrationUsageLastSeen[sessionID];
|
|
865
|
+
if (sessionID === rootId) {
|
|
866
|
+
for (const id of deleteBundleCascade(snapshot, rootId)) {
|
|
867
|
+
delete sessionTreeStore[id];
|
|
868
|
+
}
|
|
869
|
+
return;
|
|
870
|
+
}
|
|
871
|
+
const node = bundle.tree[sessionID];
|
|
872
|
+
const parentId = node?.parentId;
|
|
873
|
+
delete bundle.tree[sessionID];
|
|
874
|
+
delete sessionTreeStore[sessionID];
|
|
875
|
+
if (parentId) {
|
|
876
|
+
const parent = bundle.tree[parentId];
|
|
877
|
+
if (parent) {
|
|
878
|
+
parent.childIds = parent.childIds.filter((c) => c !== sessionID);
|
|
879
|
+
}
|
|
880
|
+
const storeParent = sessionTreeStore[parentId];
|
|
881
|
+
if (storeParent?.childIds) {
|
|
882
|
+
storeParent.childIds = storeParent.childIds.filter((c) => c !== sessionID);
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
if (Object.keys(bundle.tree).length === 0) {
|
|
886
|
+
delete snapshot.sessions[rootId];
|
|
887
|
+
return;
|
|
888
|
+
}
|
|
889
|
+
touchBundle(bundle);
|
|
890
|
+
});
|
|
891
|
+
}
|
|
892
|
+
function recordActiveSubscriptionForProvider(provider, name) {
|
|
893
|
+
updateSnapshot((snapshot) => {
|
|
894
|
+
if (name) {
|
|
895
|
+
snapshot.activeSubscriptionByProvider[provider] = name;
|
|
896
|
+
} else {
|
|
897
|
+
delete snapshot.activeSubscriptionByProvider[provider];
|
|
898
|
+
}
|
|
899
|
+
});
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
// src/tui.ts
|
|
903
|
+
var PLUGIN_NAME = "opencode-dux";
|
|
904
|
+
var BORDER = { type: "single" };
|
|
905
|
+
var SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
906
|
+
var AGENT_SORT_PRIORITY = {
|
|
907
|
+
orchestrator: 0,
|
|
908
|
+
explorer: 1,
|
|
909
|
+
librarian: 2,
|
|
910
|
+
steward: 3,
|
|
911
|
+
fixer: 4,
|
|
912
|
+
oracle: 5,
|
|
913
|
+
designer: 6,
|
|
914
|
+
interpreter: 7
|
|
915
|
+
};
|
|
916
|
+
var SIDEBAR_MODEL_DISPLAY_MAX = 20;
|
|
917
|
+
var ORCH_ROOT_TITLE_DISPLAY_MAX = 22;
|
|
918
|
+
var ORCH_ROOT_SESSION_ID_DISPLAY_MAX = 27;
|
|
919
|
+
var ORCH_ROOT_MODEL_DISPLAY_MAX = 28;
|
|
920
|
+
var ORCH_CHILD_MODEL_DISPLAY_MAX = 22;
|
|
921
|
+
var ORCH_DEFAULT_TITLE_LABEL = "New session";
|
|
922
|
+
function element(tag, props, children = []) {
|
|
923
|
+
const node = createElement(tag);
|
|
924
|
+
for (const [key, value] of Object.entries(props)) {
|
|
925
|
+
if (value !== undefined)
|
|
926
|
+
setProp(node, key, value);
|
|
927
|
+
}
|
|
928
|
+
for (const child of children) {
|
|
929
|
+
if (child === null || child === undefined || child === false)
|
|
930
|
+
continue;
|
|
931
|
+
insert(node, child);
|
|
932
|
+
}
|
|
933
|
+
return node;
|
|
934
|
+
}
|
|
935
|
+
function text(props, children) {
|
|
936
|
+
return element("text", props, children);
|
|
937
|
+
}
|
|
938
|
+
function box(props, children = []) {
|
|
939
|
+
return element("box", props, children);
|
|
940
|
+
}
|
|
941
|
+
function truncate(value, max = 24) {
|
|
942
|
+
return value.length > max ? `${value.slice(0, max - 1)}…` : value;
|
|
943
|
+
}
|
|
944
|
+
function formatTokenAbbrev(value) {
|
|
945
|
+
if (!Number.isFinite(value) || value <= 0)
|
|
946
|
+
return "0";
|
|
947
|
+
if (value < 1000)
|
|
948
|
+
return Math.round(value).toString();
|
|
949
|
+
if (value < 1e6) {
|
|
950
|
+
const k = Math.round(value / 1000);
|
|
951
|
+
if (k >= 1000)
|
|
952
|
+
return `${Math.round(value / 1e6)}M`;
|
|
953
|
+
return `${k}K`;
|
|
954
|
+
}
|
|
955
|
+
return `${Math.round(value / 1e6)}M`;
|
|
956
|
+
}
|
|
957
|
+
function formatTokenAbbrevDecimal(value) {
|
|
958
|
+
if (!Number.isFinite(value) || value <= 0)
|
|
959
|
+
return "0";
|
|
960
|
+
if (value < 1000)
|
|
961
|
+
return Math.round(value).toString();
|
|
962
|
+
if (value < 1e6) {
|
|
963
|
+
return `${(value / 1000).toFixed(1)}K`;
|
|
964
|
+
}
|
|
965
|
+
return `${(value / 1e6).toFixed(1)}M`;
|
|
966
|
+
}
|
|
967
|
+
function formatTokenExact(value) {
|
|
968
|
+
if (!Number.isFinite(value) || value <= 0)
|
|
969
|
+
return "0";
|
|
970
|
+
return new Intl.NumberFormat("en-US").format(Math.round(value));
|
|
971
|
+
}
|
|
972
|
+
function formatSidebarModelName(model) {
|
|
973
|
+
const lastSlash = model.lastIndexOf("/");
|
|
974
|
+
return lastSlash === -1 ? model : model.slice(lastSlash + 1);
|
|
975
|
+
}
|
|
976
|
+
var ELLIPSIS_CHAR = "…";
|
|
977
|
+
function truncateModelBasenameByHyphenSegments(name, maxTotalLen) {
|
|
978
|
+
if (name.length <= maxTotalLen)
|
|
979
|
+
return name;
|
|
980
|
+
const budget = maxTotalLen - ELLIPSIS_CHAR.length;
|
|
981
|
+
if (budget <= 0)
|
|
982
|
+
return truncate(name, maxTotalLen);
|
|
983
|
+
const parts = name.split("-").filter((p) => p.length > 0);
|
|
984
|
+
if (parts.length === 0)
|
|
985
|
+
return truncate(name, maxTotalLen);
|
|
986
|
+
const head = parts[0];
|
|
987
|
+
if (parts.length === 1) {
|
|
988
|
+
return head ? truncate(head, maxTotalLen) : truncate(name, maxTotalLen);
|
|
989
|
+
}
|
|
990
|
+
if (!head)
|
|
991
|
+
return truncate(name, maxTotalLen);
|
|
992
|
+
if (head.length > budget)
|
|
993
|
+
return truncate(head, maxTotalLen);
|
|
994
|
+
let acc = head;
|
|
995
|
+
for (let i = 1;i < parts.length; i++) {
|
|
996
|
+
const piece = parts[i];
|
|
997
|
+
if (!piece)
|
|
998
|
+
continue;
|
|
999
|
+
const next = `${acc}-${piece}`;
|
|
1000
|
+
if (next.length > budget)
|
|
1001
|
+
break;
|
|
1002
|
+
acc = next;
|
|
1003
|
+
}
|
|
1004
|
+
if (acc.length >= name.length)
|
|
1005
|
+
return name;
|
|
1006
|
+
return `${acc}${ELLIPSIS_CHAR}`;
|
|
1007
|
+
}
|
|
1008
|
+
function formatSidebarModelAndVariant(rawModel, variant, maxModelDisplayLen = SIDEBAR_MODEL_DISPLAY_MAX) {
|
|
1009
|
+
const name = rawModel ? formatSidebarModelName(rawModel) : "";
|
|
1010
|
+
const extraVariant = variant?.trim() ?? "";
|
|
1011
|
+
if (!name)
|
|
1012
|
+
return extraVariant;
|
|
1013
|
+
const modelShown = truncateModelBasenameByHyphenSegments(name, maxModelDisplayLen);
|
|
1014
|
+
if (!extraVariant)
|
|
1015
|
+
return modelShown;
|
|
1016
|
+
return `${modelShown} - ${extraVariant}`;
|
|
1017
|
+
}
|
|
1018
|
+
function formatAgentName(name) {
|
|
1019
|
+
if (name.length <= 16)
|
|
1020
|
+
return name;
|
|
1021
|
+
return `${name.slice(0, 13)}...`;
|
|
1022
|
+
}
|
|
1023
|
+
function formatDuration(ms) {
|
|
1024
|
+
if (!Number.isFinite(ms) || ms < 0)
|
|
1025
|
+
return "0:00";
|
|
1026
|
+
const totalSeconds = Math.floor(ms / 1000);
|
|
1027
|
+
const hours = Math.floor(totalSeconds / 3600);
|
|
1028
|
+
const minutes = Math.floor(totalSeconds % 3600 / 60);
|
|
1029
|
+
const seconds = totalSeconds % 60;
|
|
1030
|
+
if (hours > 0) {
|
|
1031
|
+
return `${hours}:${String(minutes).padStart(2, "0")}:${String(seconds).padStart(2, "0")}`;
|
|
1032
|
+
}
|
|
1033
|
+
return `${minutes}:${String(seconds).padStart(2, "0")}`;
|
|
1034
|
+
}
|
|
1035
|
+
function formatSessionUsageRows(snapshot, sessionID, options) {
|
|
1036
|
+
const abbreviateLeft = options?.abbreviateLeft ?? false;
|
|
1037
|
+
const usage = mergedSessionUsage(snapshot)[sessionID];
|
|
1038
|
+
const contextUsed = usage?.contextUsed ?? 0;
|
|
1039
|
+
const contextLimit = usage?.contextLimit ?? 0;
|
|
1040
|
+
const contextPct = Math.round(deriveSessionContextPct(contextUsed, contextLimit));
|
|
1041
|
+
const inputTotal = usage?.input ?? 0;
|
|
1042
|
+
const outputTotal = usage?.output ?? 0;
|
|
1043
|
+
const cacheRead = usage?.cacheRead ?? 0;
|
|
1044
|
+
const cacheWrite = usage?.cacheWrite ?? 0;
|
|
1045
|
+
const cacheTotal = cacheRead + cacheWrite;
|
|
1046
|
+
return {
|
|
1047
|
+
contextPct,
|
|
1048
|
+
ctxLabel: "CTX",
|
|
1049
|
+
ctxValue: `${abbreviateLeft ? formatTokenAbbrevDecimal(contextUsed) : formatTokenExact(contextUsed)}/${abbreviateLeft ? formatTokenAbbrev(contextLimit) : formatTokenExact(contextLimit)} (${contextPct}%)`,
|
|
1050
|
+
ioInputAbbrev: formatTokenAbbrev(inputTotal),
|
|
1051
|
+
ioOutputAbbrev: formatTokenAbbrev(outputTotal),
|
|
1052
|
+
cacheLabel: "CACHE",
|
|
1053
|
+
cacheValue: formatTokenExact(cacheTotal),
|
|
1054
|
+
cacheReadAbbrev: formatTokenExact(cacheRead),
|
|
1055
|
+
cacheWriteAbbrev: formatTokenExact(cacheWrite)
|
|
1056
|
+
};
|
|
1057
|
+
}
|
|
1058
|
+
function aggregateOrchestrationUsage(snapshot, rootSessionID) {
|
|
1059
|
+
const accum = mergedOrchestrationSigmaAccum(snapshot)[rootSessionID];
|
|
1060
|
+
if (!accum) {
|
|
1061
|
+
return {
|
|
1062
|
+
inputTotal: 0,
|
|
1063
|
+
outputTotal: 0,
|
|
1064
|
+
cacheRead: 0,
|
|
1065
|
+
cacheWrite: 0,
|
|
1066
|
+
contextUsed: 0
|
|
1067
|
+
};
|
|
1068
|
+
}
|
|
1069
|
+
return {
|
|
1070
|
+
inputTotal: accum.input,
|
|
1071
|
+
outputTotal: accum.output,
|
|
1072
|
+
cacheRead: accum.cacheRead,
|
|
1073
|
+
cacheWrite: accum.cacheWrite,
|
|
1074
|
+
contextUsed: accum.contextUsed
|
|
1075
|
+
};
|
|
1076
|
+
}
|
|
1077
|
+
function getSidebarAgentNames(snapshot) {
|
|
1078
|
+
const names = Object.keys(AGENT_SIDEBAR_DESCRIPTIONS);
|
|
1079
|
+
return names.sort((a, b) => {
|
|
1080
|
+
const pa = AGENT_SORT_PRIORITY[a] ?? 99;
|
|
1081
|
+
const pb = AGENT_SORT_PRIORITY[b] ?? 99;
|
|
1082
|
+
if (pa !== pb)
|
|
1083
|
+
return pa - pb;
|
|
1084
|
+
return a.localeCompare(b);
|
|
1085
|
+
});
|
|
1086
|
+
}
|
|
1087
|
+
function formatUsageTime(iso) {
|
|
1088
|
+
const diff = new Date(iso).getTime() - Date.now();
|
|
1089
|
+
if (diff <= 0)
|
|
1090
|
+
return "now";
|
|
1091
|
+
const totalMin = Math.ceil(diff / 60000);
|
|
1092
|
+
const hours = Math.floor(totalMin / 60);
|
|
1093
|
+
const minutes = totalMin % 60;
|
|
1094
|
+
if (hours > 24) {
|
|
1095
|
+
const days = Math.floor(hours / 24);
|
|
1096
|
+
return `${days}d ${hours % 24}h`;
|
|
1097
|
+
}
|
|
1098
|
+
if (hours > 0)
|
|
1099
|
+
return `${hours}h ${minutes}m`;
|
|
1100
|
+
return `${minutes}m`;
|
|
1101
|
+
}
|
|
1102
|
+
function neuralwattTokensFormatted(tokens) {
|
|
1103
|
+
if (!Number.isFinite(tokens))
|
|
1104
|
+
return "0";
|
|
1105
|
+
return Math.trunc(tokens).toLocaleString("en-US");
|
|
1106
|
+
}
|
|
1107
|
+
function pushNeuralwattMonthlyTokensRow(rows, theme, u) {
|
|
1108
|
+
rows.push(box({ width: "100%", flexDirection: "row" }, [
|
|
1109
|
+
text({ fg: theme.textMuted }, [
|
|
1110
|
+
` ${neuralwattTokensFormatted(u.current_month.tokens)} Tokens this month`
|
|
1111
|
+
])
|
|
1112
|
+
]));
|
|
1113
|
+
}
|
|
1114
|
+
var BAR_WIDTH = 18;
|
|
1115
|
+
var SIGMA_TOTAL_COLOR = "#F5B041";
|
|
1116
|
+
var METRIC_PAIR_GAP = " ";
|
|
1117
|
+
function renderMetricPairRight(leftIcon, leftValue, rightIcon, rightValue, colors) {
|
|
1118
|
+
return box({ flexDirection: "row", flexShrink: 0 }, [
|
|
1119
|
+
text({ fg: colors.leftFg }, [`${leftIcon} ${leftValue}`]),
|
|
1120
|
+
text({ fg: colors.gapFg }, [METRIC_PAIR_GAP]),
|
|
1121
|
+
text({ fg: colors.rightFg }, [`${rightIcon} ${rightValue}`])
|
|
1122
|
+
]);
|
|
1123
|
+
}
|
|
1124
|
+
function renderUsageBar(percent) {
|
|
1125
|
+
const filled = Math.round(percent / 100 * BAR_WIDTH);
|
|
1126
|
+
const empty = BAR_WIDTH - filled;
|
|
1127
|
+
return "█".repeat(filled) + "░".repeat(empty);
|
|
1128
|
+
}
|
|
1129
|
+
function getUsageColor(percentRemaining) {
|
|
1130
|
+
if (percentRemaining < 25)
|
|
1131
|
+
return "#E74C3C";
|
|
1132
|
+
if (percentRemaining < 50)
|
|
1133
|
+
return "#F39C12";
|
|
1134
|
+
return "";
|
|
1135
|
+
}
|
|
1136
|
+
function renderOpenCodeGoBars(entry, rows, theme) {
|
|
1137
|
+
const windows = [];
|
|
1138
|
+
if (entry.rolling)
|
|
1139
|
+
windows.push({ label: "R", w: entry.rolling });
|
|
1140
|
+
if (entry.weekly)
|
|
1141
|
+
windows.push({ label: "W", w: entry.weekly });
|
|
1142
|
+
if (entry.monthly)
|
|
1143
|
+
windows.push({ label: "M", w: entry.monthly });
|
|
1144
|
+
for (let i = 0;i < windows.length; i++) {
|
|
1145
|
+
const { label, w } = windows[i];
|
|
1146
|
+
if (!w)
|
|
1147
|
+
continue;
|
|
1148
|
+
const usageColor = getUsageColor(w.percentRemaining);
|
|
1149
|
+
const bar = renderUsageBar(w.percentRemaining);
|
|
1150
|
+
const pct = w.percentRemaining.toFixed(0).padStart(3);
|
|
1151
|
+
const timeLeft = formatUsageTime(w.resetTimeIso);
|
|
1152
|
+
rows.push(box({
|
|
1153
|
+
width: "100%",
|
|
1154
|
+
flexDirection: "row",
|
|
1155
|
+
justifyContent: "space-between"
|
|
1156
|
+
}, [
|
|
1157
|
+
box({ flexDirection: "row" }, [
|
|
1158
|
+
text({ fg: theme.accent }, [`${label} `]),
|
|
1159
|
+
text({ fg: usageColor || theme.text }, [bar]),
|
|
1160
|
+
text({ fg: usageColor || theme.textMuted }, [` ${pct}%`])
|
|
1161
|
+
]),
|
|
1162
|
+
text({ fg: theme.textMuted }, [timeLeft])
|
|
1163
|
+
]));
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
function renderNeuralwattUsage(entry, rows, theme) {
|
|
1167
|
+
const { subscription, balance, usage: u } = entry;
|
|
1168
|
+
if (subscription && subscription.status === "active") {
|
|
1169
|
+
const kwhIncluded = subscription.kwh_included ?? 0;
|
|
1170
|
+
const kwhUsed = subscription.kwh_used ?? 0;
|
|
1171
|
+
const kwhRemaining = subscription.kwh_remaining ?? 0;
|
|
1172
|
+
if (kwhIncluded > 0) {
|
|
1173
|
+
const kwhPct = Math.min(kwhUsed / kwhIncluded * 100, 100);
|
|
1174
|
+
const bar = renderUsageBar(100 - kwhPct);
|
|
1175
|
+
const remaining = kwhRemaining.toFixed(1);
|
|
1176
|
+
const resetTime = subscription.current_period_end ? formatUsageTime(subscription.current_period_end) : "";
|
|
1177
|
+
const color = kwhPct > 90 ? "#E74C3C" : kwhPct > 75 ? "#F39C12" : "";
|
|
1178
|
+
rows.push(box({
|
|
1179
|
+
width: "100%",
|
|
1180
|
+
flexDirection: "row",
|
|
1181
|
+
justifyContent: "space-between"
|
|
1182
|
+
}, [
|
|
1183
|
+
box({ flexDirection: "row" }, [
|
|
1184
|
+
text({ fg: theme.accent }, ["⚡ "]),
|
|
1185
|
+
text({ fg: color || theme.text }, [bar]),
|
|
1186
|
+
text({ fg: color || theme.textMuted }, [` ${remaining}kWh`])
|
|
1187
|
+
]),
|
|
1188
|
+
text({ fg: theme.textMuted }, [resetTime])
|
|
1189
|
+
]));
|
|
1190
|
+
}
|
|
1191
|
+
rows.push(box({
|
|
1192
|
+
width: "100%",
|
|
1193
|
+
flexDirection: "row",
|
|
1194
|
+
justifyContent: "space-between"
|
|
1195
|
+
}, [
|
|
1196
|
+
text({ fg: theme.textMuted }, [
|
|
1197
|
+
` $${u.current_month.cost_usd.toFixed(2)} this month`
|
|
1198
|
+
]),
|
|
1199
|
+
text({ fg: theme.textMuted }, [
|
|
1200
|
+
`⚡ ${u.current_month.energy_kwh.toFixed(1)} kWh`
|
|
1201
|
+
])
|
|
1202
|
+
]));
|
|
1203
|
+
pushNeuralwattMonthlyTokensRow(rows, theme, u);
|
|
1204
|
+
} else if (subscription && subscription.status !== "active") {
|
|
1205
|
+
const statusColor = subscription.status === "past_due" || subscription.status === "canceling" ? "#E74C3C" : "#F39C12";
|
|
1206
|
+
rows.push(box({
|
|
1207
|
+
width: "100%",
|
|
1208
|
+
flexDirection: "row",
|
|
1209
|
+
justifyContent: "space-between"
|
|
1210
|
+
}, [
|
|
1211
|
+
text({ fg: statusColor }, [` Status: ${subscription.status}`]),
|
|
1212
|
+
text({ fg: theme.textMuted }, [
|
|
1213
|
+
`⚡ ${u.current_month.energy_kwh.toFixed(1)} kWh`
|
|
1214
|
+
])
|
|
1215
|
+
]));
|
|
1216
|
+
const kwhIncluded = subscription.kwh_included ?? 0;
|
|
1217
|
+
const kwhUsed = subscription.kwh_used ?? 0;
|
|
1218
|
+
const kwhRemaining = subscription.kwh_remaining ?? 0;
|
|
1219
|
+
if (kwhIncluded > 0) {
|
|
1220
|
+
const kwhPct = Math.min(kwhUsed / kwhIncluded * 100, 100);
|
|
1221
|
+
const bar = renderUsageBar(100 - kwhPct);
|
|
1222
|
+
const remaining = kwhRemaining.toFixed(1);
|
|
1223
|
+
const resetTime = subscription.current_period_end ? formatUsageTime(subscription.current_period_end) : "";
|
|
1224
|
+
const color = kwhPct > 90 ? "#E74C3C" : kwhPct > 75 ? "#F39C12" : "";
|
|
1225
|
+
rows.push(box({
|
|
1226
|
+
width: "100%",
|
|
1227
|
+
flexDirection: "row",
|
|
1228
|
+
justifyContent: "space-between"
|
|
1229
|
+
}, [
|
|
1230
|
+
box({ flexDirection: "row" }, [
|
|
1231
|
+
text({ fg: theme.accent }, ["⚡ "]),
|
|
1232
|
+
text({ fg: color || theme.text }, [bar]),
|
|
1233
|
+
text({ fg: color || theme.textMuted }, [` ${remaining}kWh`])
|
|
1234
|
+
]),
|
|
1235
|
+
text({ fg: theme.textMuted }, [resetTime])
|
|
1236
|
+
]));
|
|
1237
|
+
}
|
|
1238
|
+
if (balance.credits_remaining_usd > 0) {
|
|
1239
|
+
rows.push(box({
|
|
1240
|
+
width: "100%",
|
|
1241
|
+
flexDirection: "row",
|
|
1242
|
+
justifyContent: "space-between"
|
|
1243
|
+
}, [
|
|
1244
|
+
text({ fg: theme.textMuted }, [
|
|
1245
|
+
` \uD83D\uDCB0 $${balance.credits_remaining_usd.toFixed(2)} remaining`
|
|
1246
|
+
]),
|
|
1247
|
+
text({ fg: theme.textMuted }, [
|
|
1248
|
+
`$${u.current_month.cost_usd.toFixed(2)}/mo`
|
|
1249
|
+
])
|
|
1250
|
+
]));
|
|
1251
|
+
}
|
|
1252
|
+
pushNeuralwattMonthlyTokensRow(rows, theme, u);
|
|
1253
|
+
} else {
|
|
1254
|
+
rows.push(box({
|
|
1255
|
+
width: "100%",
|
|
1256
|
+
flexDirection: "row",
|
|
1257
|
+
justifyContent: "space-between"
|
|
1258
|
+
}, [
|
|
1259
|
+
text({ fg: theme.text }, [
|
|
1260
|
+
`\uD83D\uDCB0 $${balance.credits_remaining_usd.toFixed(2)} remaining`
|
|
1261
|
+
]),
|
|
1262
|
+
text({ fg: theme.textMuted }, [
|
|
1263
|
+
`⚡ ${u.current_month.energy_kwh.toFixed(3)} kWh/mo`
|
|
1264
|
+
])
|
|
1265
|
+
]));
|
|
1266
|
+
rows.push(box({
|
|
1267
|
+
width: "100%",
|
|
1268
|
+
flexDirection: "row",
|
|
1269
|
+
justifyContent: "space-between"
|
|
1270
|
+
}, [
|
|
1271
|
+
text({ fg: theme.textMuted }, [
|
|
1272
|
+
` $${u.current_month.cost_usd.toFixed(2)} this month`
|
|
1273
|
+
])
|
|
1274
|
+
]));
|
|
1275
|
+
pushNeuralwattMonthlyTokensRow(rows, theme, u);
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
function renderSubscriptionPanel(snapshot, theme) {
|
|
1279
|
+
const usage = snapshot.subscriptionUsage ?? {};
|
|
1280
|
+
const usageEntries = Object.entries(usage).sort(([, a], [, b]) => {
|
|
1281
|
+
if (a.provider !== b.provider)
|
|
1282
|
+
return a.provider.localeCompare(b.provider);
|
|
1283
|
+
return a.accountName.localeCompare(b.accountName);
|
|
1284
|
+
});
|
|
1285
|
+
if (usageEntries.length === 0)
|
|
1286
|
+
return [];
|
|
1287
|
+
const rows = [];
|
|
1288
|
+
let isFirstAccount = true;
|
|
1289
|
+
for (const [, entry] of usageEntries) {
|
|
1290
|
+
const name = entry.accountName;
|
|
1291
|
+
const activeName = snapshot.activeSubscriptionByProvider?.[entry.provider];
|
|
1292
|
+
const isActive = activeName === name;
|
|
1293
|
+
const providerLabel = entry.provider === "neuralwatt" ? " [nw]" : " [go]";
|
|
1294
|
+
if (!isFirstAccount) {
|
|
1295
|
+
rows.push(box({ width: "100%", height: 1 }));
|
|
1296
|
+
}
|
|
1297
|
+
isFirstAccount = false;
|
|
1298
|
+
if (entry.error) {
|
|
1299
|
+
rows.push(box({ width: "100%", flexDirection: "row" }, [
|
|
1300
|
+
text(isActive ? { fg: theme.accent } : { fg: theme.text }, [
|
|
1301
|
+
isActive ? `★ ${truncate(name, 18)}${providerLabel}` : `${truncate(name, 16)}${providerLabel}`
|
|
1302
|
+
]),
|
|
1303
|
+
text({ fg: theme.textMuted }, [" ⚠️"])
|
|
1304
|
+
]));
|
|
1305
|
+
rows.push(text({ fg: theme.textMuted }, [` ${truncate(entry.error, 56)}`]));
|
|
1306
|
+
continue;
|
|
1307
|
+
}
|
|
1308
|
+
const displayName = isActive ? `★ ${truncate(name, 18)}${providerLabel}` : `${truncate(name, 16)}${providerLabel}`;
|
|
1309
|
+
rows.push(box({ width: "100%", flexDirection: "row" }, [
|
|
1310
|
+
text(isActive ? { fg: theme.accent } : { fg: theme.text }, [
|
|
1311
|
+
displayName
|
|
1312
|
+
])
|
|
1313
|
+
]));
|
|
1314
|
+
if (entry.provider === "opencode-go") {
|
|
1315
|
+
renderOpenCodeGoBars(entry, rows, theme);
|
|
1316
|
+
} else if (entry.provider === "neuralwatt") {
|
|
1317
|
+
renderNeuralwattUsage(entry, rows, theme);
|
|
1318
|
+
} else {
|
|
1319
|
+
rows.push(text({ fg: "#F39C12" }, [
|
|
1320
|
+
" ⚠️ Provider field missing - re-add account with /subscriptions"
|
|
1321
|
+
]));
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
return rows;
|
|
1325
|
+
}
|
|
1326
|
+
var FLASH_DURATION_MS = 2000;
|
|
1327
|
+
function getStatusText(snapshot, sessionID) {
|
|
1328
|
+
return mergedSessionTree(snapshot)[sessionID]?.status ?? "-";
|
|
1329
|
+
}
|
|
1330
|
+
function getStatusWithDuration(snapshot, sessionID, node, now) {
|
|
1331
|
+
const status = getStatusText(snapshot, sessionID);
|
|
1332
|
+
if (node.status === "busy" || node.status === "retry") {
|
|
1333
|
+
const elapsed = now - node.createdAt;
|
|
1334
|
+
return `${status} (${formatDuration(elapsed)})`;
|
|
1335
|
+
}
|
|
1336
|
+
return status;
|
|
1337
|
+
}
|
|
1338
|
+
function getSpinnerChar(now) {
|
|
1339
|
+
return SPINNER_FRAMES[Math.floor(now / 80) % SPINNER_FRAMES.length];
|
|
1340
|
+
}
|
|
1341
|
+
function getStatusColor(status, theme) {
|
|
1342
|
+
const normalized = status.trim();
|
|
1343
|
+
if (normalized === "busy" || normalized.startsWith("busy "))
|
|
1344
|
+
return theme.accent;
|
|
1345
|
+
if (normalized === "retry" || normalized.startsWith("retry "))
|
|
1346
|
+
return theme.error ?? "#EF4444";
|
|
1347
|
+
if (status === "idle")
|
|
1348
|
+
return theme.textMuted;
|
|
1349
|
+
return theme.text;
|
|
1350
|
+
}
|
|
1351
|
+
function splitStatusAndTimer(full) {
|
|
1352
|
+
const m = full.match(/^(\S+)\s+(\([^)]+\))$/);
|
|
1353
|
+
if (!m)
|
|
1354
|
+
return null;
|
|
1355
|
+
return { status: m[1], timer: full.slice(m[1].length) };
|
|
1356
|
+
}
|
|
1357
|
+
function renderStatusLineWithOptionalTimer(full, theme) {
|
|
1358
|
+
const split = splitStatusAndTimer(full);
|
|
1359
|
+
if (!split) {
|
|
1360
|
+
return text({ fg: getStatusColor(full, theme) }, [full]);
|
|
1361
|
+
}
|
|
1362
|
+
return box({ flexDirection: "row", flexShrink: 0 }, [
|
|
1363
|
+
text({ fg: getStatusColor(split.status, theme) }, [split.status]),
|
|
1364
|
+
text({ fg: theme.text }, [split.timer])
|
|
1365
|
+
]);
|
|
1366
|
+
}
|
|
1367
|
+
function buildOrchestratingRows(snapshot, now, theme) {
|
|
1368
|
+
const tree = mergedSessionTree(snapshot);
|
|
1369
|
+
const usageBySession = mergedSessionUsage(snapshot);
|
|
1370
|
+
const spinner = getSpinnerChar(now);
|
|
1371
|
+
const isVisibleSession = (node) => {
|
|
1372
|
+
if (node.status === "busy" || node.status === "retry")
|
|
1373
|
+
return true;
|
|
1374
|
+
if (node.status !== "idle" || !node.finishedAt)
|
|
1375
|
+
return false;
|
|
1376
|
+
return now - node.finishedAt < FLASH_DURATION_MS + 1000;
|
|
1377
|
+
};
|
|
1378
|
+
const getVisibleChildren = (parentID) => Object.entries(tree).filter(([, child]) => child.parentId === parentID && isVisibleSession(child));
|
|
1379
|
+
const pushUsageRows = (rows2, sessionID, prefix, abbreviateLeft) => {
|
|
1380
|
+
const metrics = formatSessionUsageRows(snapshot, sessionID, {
|
|
1381
|
+
abbreviateLeft
|
|
1382
|
+
});
|
|
1383
|
+
const isChild = !!tree[sessionID]?.parentId;
|
|
1384
|
+
if (isChild) {
|
|
1385
|
+
rows2.push(box({ width: "100%", flexDirection: "row" }, [
|
|
1386
|
+
text({ fg: theme.textMuted }, [prefix]),
|
|
1387
|
+
text({ fg: theme.accent }, [`${metrics.ctxLabel} `]),
|
|
1388
|
+
text({ fg: theme.text }, [metrics.ctxValue])
|
|
1389
|
+
]));
|
|
1390
|
+
const cacheTotalForRow = (usageBySession[sessionID]?.cacheRead ?? 0) + (usageBySession[sessionID]?.cacheWrite ?? 0);
|
|
1391
|
+
rows2.push(box({ width: "100%", flexDirection: "row" }, [
|
|
1392
|
+
text({ fg: theme.textMuted }, [prefix]),
|
|
1393
|
+
text({ fg: theme.accent }, [`${metrics.cacheLabel} `]),
|
|
1394
|
+
text({ fg: theme.text }, [formatTokenExact(cacheTotalForRow)])
|
|
1395
|
+
]));
|
|
1396
|
+
rows2.push(box({ width: "100%", flexDirection: "row" }, [
|
|
1397
|
+
text({ fg: theme.textMuted }, [prefix]),
|
|
1398
|
+
renderMetricPairRight("↓", `Input ${metrics.ioInputAbbrev}`, "↑", `Output ${metrics.ioOutputAbbrev}`, {
|
|
1399
|
+
leftFg: "#5DADE2",
|
|
1400
|
+
rightFg: "#58D68D",
|
|
1401
|
+
gapFg: theme.textMuted
|
|
1402
|
+
})
|
|
1403
|
+
]));
|
|
1404
|
+
rows2.push(box({ width: "100%", flexDirection: "row" }, [
|
|
1405
|
+
text({ fg: theme.textMuted }, [prefix]),
|
|
1406
|
+
renderMetricPairRight("\uD83D\uDCD6", `Read ${metrics.cacheReadAbbrev}`, "\uD83D\uDCDD", `Write ${metrics.cacheWriteAbbrev}`, {
|
|
1407
|
+
leftFg: "#5DADE2",
|
|
1408
|
+
rightFg: "#AF7AC5",
|
|
1409
|
+
gapFg: theme.textMuted
|
|
1410
|
+
})
|
|
1411
|
+
]));
|
|
1412
|
+
} else {
|
|
1413
|
+
rows2.push(box({
|
|
1414
|
+
width: "100%",
|
|
1415
|
+
flexDirection: "row",
|
|
1416
|
+
justifyContent: "space-between"
|
|
1417
|
+
}, [
|
|
1418
|
+
box({ flexDirection: "row" }, [
|
|
1419
|
+
text({ fg: theme.textMuted }, [prefix]),
|
|
1420
|
+
text({ fg: theme.accent }, [`${metrics.ctxLabel} `]),
|
|
1421
|
+
text({ fg: theme.text }, [metrics.ctxValue])
|
|
1422
|
+
]),
|
|
1423
|
+
renderMetricPairRight("↓", metrics.ioInputAbbrev, "↑", metrics.ioOutputAbbrev, {
|
|
1424
|
+
leftFg: "#5DADE2",
|
|
1425
|
+
rightFg: "#58D68D",
|
|
1426
|
+
gapFg: theme.textMuted
|
|
1427
|
+
})
|
|
1428
|
+
]));
|
|
1429
|
+
rows2.push(box({
|
|
1430
|
+
width: "100%",
|
|
1431
|
+
flexDirection: "row",
|
|
1432
|
+
justifyContent: "space-between"
|
|
1433
|
+
}, [
|
|
1434
|
+
box({ flexDirection: "row" }, [
|
|
1435
|
+
text({ fg: theme.textMuted }, [prefix]),
|
|
1436
|
+
text({ fg: theme.accent }, [`${metrics.cacheLabel} `]),
|
|
1437
|
+
text({ fg: theme.text }, [metrics.cacheValue])
|
|
1438
|
+
]),
|
|
1439
|
+
renderMetricPairRight("\uD83D\uDCD6", metrics.cacheReadAbbrev, "\uD83D\uDCDD", metrics.cacheWriteAbbrev, {
|
|
1440
|
+
leftFg: "#5DADE2",
|
|
1441
|
+
rightFg: "#AF7AC5",
|
|
1442
|
+
gapFg: theme.textMuted
|
|
1443
|
+
})
|
|
1444
|
+
]));
|
|
1445
|
+
}
|
|
1446
|
+
};
|
|
1447
|
+
const pushAggregateRows = (rows2, sessionID, prefix) => {
|
|
1448
|
+
const totals = aggregateOrchestrationUsage(snapshot, sessionID);
|
|
1449
|
+
const totalIo = totals.contextUsed;
|
|
1450
|
+
const totalCache = totals.cacheRead + totals.cacheWrite;
|
|
1451
|
+
const isChild = !!tree[sessionID]?.parentId;
|
|
1452
|
+
if (isChild) {
|
|
1453
|
+
rows2.push(box({ width: "100%", flexDirection: "row" }, [
|
|
1454
|
+
text({ fg: theme.textMuted }, [prefix]),
|
|
1455
|
+
text({ fg: SIGMA_TOTAL_COLOR }, ["Σ TOTAL "]),
|
|
1456
|
+
text({ fg: theme.text }, [formatTokenExact(totalIo)])
|
|
1457
|
+
]));
|
|
1458
|
+
rows2.push(box({ width: "100%", flexDirection: "row" }, [
|
|
1459
|
+
text({ fg: theme.textMuted }, [prefix]),
|
|
1460
|
+
text({ fg: SIGMA_TOTAL_COLOR }, ["Σ CACHE "]),
|
|
1461
|
+
text({ fg: theme.text }, [formatTokenAbbrev(totalCache)])
|
|
1462
|
+
]));
|
|
1463
|
+
rows2.push(box({ width: "100%", flexDirection: "row" }, [
|
|
1464
|
+
text({ fg: theme.textMuted }, [prefix]),
|
|
1465
|
+
renderMetricPairRight("↓", `Input ${formatTokenAbbrev(totals.inputTotal)}`, "↑", `Output ${formatTokenAbbrev(totals.outputTotal)}`, {
|
|
1466
|
+
leftFg: "#5DADE2",
|
|
1467
|
+
rightFg: "#58D68D",
|
|
1468
|
+
gapFg: theme.textMuted
|
|
1469
|
+
})
|
|
1470
|
+
]));
|
|
1471
|
+
rows2.push(box({ width: "100%", flexDirection: "row" }, [
|
|
1472
|
+
text({ fg: theme.textMuted }, [prefix]),
|
|
1473
|
+
renderMetricPairRight("\uD83D\uDCD6", `Read ${formatTokenAbbrev(totals.cacheRead)}`, "\uD83D\uDCDD", `Write ${formatTokenAbbrev(totals.cacheWrite)}`, {
|
|
1474
|
+
leftFg: "#5DADE2",
|
|
1475
|
+
rightFg: "#AF7AC5",
|
|
1476
|
+
gapFg: theme.textMuted
|
|
1477
|
+
})
|
|
1478
|
+
]));
|
|
1479
|
+
} else {
|
|
1480
|
+
rows2.push(box({
|
|
1481
|
+
width: "100%",
|
|
1482
|
+
flexDirection: "row",
|
|
1483
|
+
justifyContent: "space-between"
|
|
1484
|
+
}, [
|
|
1485
|
+
box({ flexDirection: "row" }, [
|
|
1486
|
+
text({ fg: theme.textMuted }, [prefix]),
|
|
1487
|
+
text({ fg: SIGMA_TOTAL_COLOR }, ["Σ TOTAL "]),
|
|
1488
|
+
text({ fg: theme.text }, [formatTokenExact(totalIo)])
|
|
1489
|
+
]),
|
|
1490
|
+
renderMetricPairRight("↓", formatTokenAbbrev(totals.inputTotal), "↑", formatTokenAbbrev(totals.outputTotal), {
|
|
1491
|
+
leftFg: "#5DADE2",
|
|
1492
|
+
rightFg: "#58D68D",
|
|
1493
|
+
gapFg: theme.textMuted
|
|
1494
|
+
})
|
|
1495
|
+
]));
|
|
1496
|
+
rows2.push(box({
|
|
1497
|
+
width: "100%",
|
|
1498
|
+
flexDirection: "row",
|
|
1499
|
+
justifyContent: "space-between"
|
|
1500
|
+
}, [
|
|
1501
|
+
box({ flexDirection: "row" }, [
|
|
1502
|
+
text({ fg: theme.textMuted }, [prefix]),
|
|
1503
|
+
text({ fg: SIGMA_TOTAL_COLOR }, ["Σ CACHE "]),
|
|
1504
|
+
text({ fg: theme.text }, [formatTokenExact(totalCache)])
|
|
1505
|
+
]),
|
|
1506
|
+
renderMetricPairRight("\uD83D\uDCD6", formatTokenAbbrev(totals.cacheRead), "\uD83D\uDCDD", formatTokenAbbrev(totals.cacheWrite), {
|
|
1507
|
+
leftFg: "#5DADE2",
|
|
1508
|
+
rightFg: "#AF7AC5",
|
|
1509
|
+
gapFg: theme.textMuted
|
|
1510
|
+
})
|
|
1511
|
+
]));
|
|
1512
|
+
}
|
|
1513
|
+
};
|
|
1514
|
+
const visibleOrchSessions = [];
|
|
1515
|
+
for (const [id, node] of Object.entries(tree)) {
|
|
1516
|
+
if (node.agent !== "orchestrator")
|
|
1517
|
+
continue;
|
|
1518
|
+
if (node.status === "busy" || node.status === "retry") {
|
|
1519
|
+
visibleOrchSessions.push([id, node]);
|
|
1520
|
+
} else if (node.status === "idle") {
|
|
1521
|
+
const hasVisibleChildren = getVisibleChildren(id).length > 0;
|
|
1522
|
+
if (hasVisibleChildren) {
|
|
1523
|
+
visibleOrchSessions.push([id, node]);
|
|
1524
|
+
} else if (node.finishedAt) {
|
|
1525
|
+
const elapsed = now - node.finishedAt;
|
|
1526
|
+
if (elapsed < FLASH_DURATION_MS + 1000) {
|
|
1527
|
+
visibleOrchSessions.push([id, node]);
|
|
1528
|
+
}
|
|
1529
|
+
} else {
|
|
1530
|
+
visibleOrchSessions.push([id, node]);
|
|
1531
|
+
}
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1534
|
+
const countLabel = `${visibleOrchSessions.length} active`;
|
|
1535
|
+
if (visibleOrchSessions.length === 0) {
|
|
1536
|
+
return [
|
|
1537
|
+
countLabel,
|
|
1538
|
+
text({ fg: theme.textMuted }, ["No active orchestrations"])
|
|
1539
|
+
];
|
|
1540
|
+
}
|
|
1541
|
+
const rows = [];
|
|
1542
|
+
const renderChildren = (parentID, indentPrefix) => {
|
|
1543
|
+
const visibleChildren = getVisibleChildren(parentID);
|
|
1544
|
+
for (let i = 0;i < visibleChildren.length; i++) {
|
|
1545
|
+
const [childId, child] = visibleChildren[i];
|
|
1546
|
+
const isLast = i === visibleChildren.length - 1;
|
|
1547
|
+
const branchChar = isLast ? "└" : "├";
|
|
1548
|
+
const pipeChar = isLast ? " " : "│";
|
|
1549
|
+
const childFlash = child.status === "idle" && child.finishedAt && Math.floor((now - child.finishedAt) / 200) % 2 === 0;
|
|
1550
|
+
const indicator = child.status === "busy" || child.status === "retry" ? spinner : childFlash ? "·" : " ";
|
|
1551
|
+
const childStatusText = getStatusWithDuration(snapshot, childId, child, now);
|
|
1552
|
+
const childVariant = child.variant;
|
|
1553
|
+
const detailPrefix = `${indentPrefix}${pipeChar} `;
|
|
1554
|
+
rows.push(box({
|
|
1555
|
+
width: "100%",
|
|
1556
|
+
flexDirection: "row",
|
|
1557
|
+
justifyContent: "space-between"
|
|
1558
|
+
}, [
|
|
1559
|
+
box({ flexDirection: "row", flexShrink: 0 }, [
|
|
1560
|
+
text({ fg: theme.textMuted }, [`${indentPrefix}${branchChar}─ `]),
|
|
1561
|
+
text({ fg: theme.text }, [`${indicator} ${child.agent}`])
|
|
1562
|
+
]),
|
|
1563
|
+
renderStatusLineWithOptionalTimer(childStatusText, theme)
|
|
1564
|
+
]));
|
|
1565
|
+
rows.push(box({ width: "100%", flexDirection: "row" }, [
|
|
1566
|
+
text({ fg: theme.textMuted }, [detailPrefix]),
|
|
1567
|
+
text({ fg: theme.textMuted }, [
|
|
1568
|
+
formatSidebarModelAndVariant(child.model, childVariant, ORCH_CHILD_MODEL_DISPLAY_MAX)
|
|
1569
|
+
])
|
|
1570
|
+
]));
|
|
1571
|
+
pushUsageRows(rows, childId, detailPrefix, false);
|
|
1572
|
+
renderChildren(childId, `${indentPrefix}${pipeChar} `);
|
|
1573
|
+
}
|
|
1574
|
+
};
|
|
1575
|
+
for (const [orchId, orchNode] of visibleOrchSessions) {
|
|
1576
|
+
const visibleChildren = getVisibleChildren(orchId);
|
|
1577
|
+
const orchShowSpinner = orchNode.status === "busy" || orchNode.status === "retry" || orchNode.status === "idle" && visibleChildren.length > 0;
|
|
1578
|
+
const orchFlash = orchNode.status === "idle" && !orchShowSpinner && orchNode.finishedAt && now >= orchNode.finishedAt && Math.floor((now - orchNode.finishedAt) / 200) % 2 === 0;
|
|
1579
|
+
const orchDot = orchShowSpinner ? spinner : orchFlash ? "·" : " ";
|
|
1580
|
+
const row1Title = orchNode.title?.trim() ? truncate(orchNode.title, ORCH_ROOT_TITLE_DISPLAY_MAX) : ORCH_DEFAULT_TITLE_LABEL;
|
|
1581
|
+
rows.push(box({
|
|
1582
|
+
flexDirection: "row",
|
|
1583
|
+
justifyContent: "space-between"
|
|
1584
|
+
}, [
|
|
1585
|
+
box({ flexDirection: "row" }, [
|
|
1586
|
+
text({ fg: theme.accent }, [`${orchDot} `]),
|
|
1587
|
+
text({ fg: theme.text }, [row1Title])
|
|
1588
|
+
]),
|
|
1589
|
+
text({ fg: theme.text }, [
|
|
1590
|
+
orchNode.status === "busy" || orchNode.status === "retry" ? `(${formatDuration(now - orchNode.createdAt)})` : ""
|
|
1591
|
+
])
|
|
1592
|
+
]));
|
|
1593
|
+
const orchStatusText = getStatusText(snapshot, orchId);
|
|
1594
|
+
rows.push(box({
|
|
1595
|
+
width: "100%",
|
|
1596
|
+
flexDirection: "row",
|
|
1597
|
+
justifyContent: "space-between"
|
|
1598
|
+
}, [
|
|
1599
|
+
box({ flexDirection: "row", flexShrink: 0 }, [
|
|
1600
|
+
text({ fg: theme.textMuted }, [" "]),
|
|
1601
|
+
text({ fg: theme.text }, [
|
|
1602
|
+
truncate(orchId, ORCH_ROOT_SESSION_ID_DISPLAY_MAX)
|
|
1603
|
+
])
|
|
1604
|
+
]),
|
|
1605
|
+
renderStatusLineWithOptionalTimer(orchStatusText, theme)
|
|
1606
|
+
]));
|
|
1607
|
+
const modelLine = formatSidebarModelAndVariant(orchNode.model, orchNode.variant, ORCH_ROOT_MODEL_DISPLAY_MAX);
|
|
1608
|
+
rows.push(box({ width: "100%", flexDirection: "row" }, [
|
|
1609
|
+
text({ fg: theme.textMuted }, [" "]),
|
|
1610
|
+
text({ fg: theme.textMuted }, [
|
|
1611
|
+
modelLine.length > 0 ? modelLine : "pending"
|
|
1612
|
+
])
|
|
1613
|
+
]));
|
|
1614
|
+
pushUsageRows(rows, orchId, " ", true);
|
|
1615
|
+
pushAggregateRows(rows, orchId, " ");
|
|
1616
|
+
renderChildren(orchId, " ");
|
|
1617
|
+
rows.push(box({ width: "100%", height: 1 }));
|
|
1618
|
+
}
|
|
1619
|
+
return [countLabel, ...rows];
|
|
1620
|
+
}
|
|
1621
|
+
function getActiveSessions(snapshot, now) {
|
|
1622
|
+
const entries = [];
|
|
1623
|
+
const tree = mergedSessionTree(snapshot);
|
|
1624
|
+
for (const [sessionID, node] of Object.entries(tree)) {
|
|
1625
|
+
const agentName = node.agent;
|
|
1626
|
+
if (!agentName)
|
|
1627
|
+
continue;
|
|
1628
|
+
if (node.status === "busy" || node.status === "retry") {
|
|
1629
|
+
entries.push({ sessionID, agentName, running: true, finished: false });
|
|
1630
|
+
} else if (node.status === "idle" && node.finishedAt) {
|
|
1631
|
+
let running = false;
|
|
1632
|
+
if (agentName === "orchestrator") {
|
|
1633
|
+
const hasVisibleChildren = Object.entries(tree).some(([_cid, cnode]) => cnode.parentId === sessionID && (cnode.status === "busy" || cnode.status === "retry" || cnode.status === "idle" && cnode.finishedAt && now - cnode.finishedAt < FLASH_DURATION_MS + 1000));
|
|
1634
|
+
if (hasVisibleChildren)
|
|
1635
|
+
running = true;
|
|
1636
|
+
}
|
|
1637
|
+
if (now - node.finishedAt < FLASH_DURATION_MS + 1000) {
|
|
1638
|
+
entries.push({
|
|
1639
|
+
sessionID,
|
|
1640
|
+
agentName,
|
|
1641
|
+
running,
|
|
1642
|
+
finished: !running
|
|
1643
|
+
});
|
|
1644
|
+
}
|
|
1645
|
+
}
|
|
1646
|
+
}
|
|
1647
|
+
return entries;
|
|
1648
|
+
}
|
|
1649
|
+
function renderSidebar(snapshot, theme) {
|
|
1650
|
+
const now = Date.now();
|
|
1651
|
+
const mergedTreeSidebar = mergedSessionTree(snapshot);
|
|
1652
|
+
const sessions = getActiveSessions(snapshot, now);
|
|
1653
|
+
const totalActive = sessions.filter((s) => s.running).length;
|
|
1654
|
+
const spinner = getSpinnerChar(now);
|
|
1655
|
+
const ourSessions = sessions.filter((s) => (s.agentName in AGENT_SORT_PRIORITY)).sort((a, b) => {
|
|
1656
|
+
const pa = AGENT_SORT_PRIORITY[a.agentName] ?? 99;
|
|
1657
|
+
const pb = AGENT_SORT_PRIORITY[b.agentName] ?? 99;
|
|
1658
|
+
if (pa !== pb)
|
|
1659
|
+
return pa - pb;
|
|
1660
|
+
return a.agentName.localeCompare(b.agentName);
|
|
1661
|
+
});
|
|
1662
|
+
const customSessions = sessions.filter((s) => !(s.agentName in AGENT_SORT_PRIORITY)).sort((a, b) => a.agentName.localeCompare(b.agentName));
|
|
1663
|
+
const agentRows = [];
|
|
1664
|
+
const ourGroups = new Map;
|
|
1665
|
+
for (const entry of ourSessions) {
|
|
1666
|
+
const { sessionID, agentName, running, finished } = entry;
|
|
1667
|
+
const rawModel = mergedTreeSidebar[sessionID]?.model;
|
|
1668
|
+
const model = rawModel ? formatSidebarModelName(rawModel) : "pending";
|
|
1669
|
+
const variant = mergedTreeSidebar[sessionID]?.variant;
|
|
1670
|
+
const key = `${agentName}\x00${model}\x00${variant ?? ""}`;
|
|
1671
|
+
const group = ourGroups.get(key);
|
|
1672
|
+
if (group) {
|
|
1673
|
+
group.count++;
|
|
1674
|
+
group.running = group.running || running;
|
|
1675
|
+
group.finished = group.finished || finished;
|
|
1676
|
+
} else {
|
|
1677
|
+
ourGroups.set(key, {
|
|
1678
|
+
sessionID,
|
|
1679
|
+
agentName,
|
|
1680
|
+
running,
|
|
1681
|
+
finished,
|
|
1682
|
+
count: 1,
|
|
1683
|
+
model,
|
|
1684
|
+
variant
|
|
1685
|
+
});
|
|
1686
|
+
}
|
|
1687
|
+
}
|
|
1688
|
+
for (const entry of ourGroups.values()) {
|
|
1689
|
+
const { sessionID, agentName, running, finished, count, variant } = entry;
|
|
1690
|
+
const elapsed = finished ? now - (mergedTreeSidebar[sessionID]?.finishedAt ?? 0) : 0;
|
|
1691
|
+
const flashDot = finished && Math.floor(elapsed / 200) % 2 === 0;
|
|
1692
|
+
const indicator = running ? spinner : flashDot ? "·" : " ";
|
|
1693
|
+
const desc = AGENT_SIDEBAR_DESCRIPTIONS[agentName] ?? agentName;
|
|
1694
|
+
const indicatorColor = theme.accent;
|
|
1695
|
+
const nameStr = formatAgentName(agentName);
|
|
1696
|
+
const descStr = truncate(desc, 10);
|
|
1697
|
+
agentRows.push(box({
|
|
1698
|
+
width: "100%",
|
|
1699
|
+
flexDirection: "row",
|
|
1700
|
+
justifyContent: "space-between"
|
|
1701
|
+
}, [
|
|
1702
|
+
box({ flexDirection: "row" }, [
|
|
1703
|
+
text({ fg: indicatorColor }, [`${indicator} `]),
|
|
1704
|
+
text({ fg: theme.text }, [nameStr]),
|
|
1705
|
+
text({ fg: theme.accent }, [` x${count}`])
|
|
1706
|
+
]),
|
|
1707
|
+
box({ flexDirection: "row" }, [text({ fg: theme.text }, [descStr])])
|
|
1708
|
+
]));
|
|
1709
|
+
const rawModel = mergedTreeSidebar[sessionID]?.model;
|
|
1710
|
+
const modelVariantLine = formatSidebarModelAndVariant(rawModel, variant);
|
|
1711
|
+
const statusText = getStatusText(snapshot, sessionID);
|
|
1712
|
+
agentRows.push(box({
|
|
1713
|
+
width: "100%",
|
|
1714
|
+
flexDirection: "row",
|
|
1715
|
+
justifyContent: "space-between"
|
|
1716
|
+
}, [
|
|
1717
|
+
text({ fg: theme.textMuted }, [
|
|
1718
|
+
modelVariantLine.length > 0 ? ` ${modelVariantLine}` : " pending"
|
|
1719
|
+
]),
|
|
1720
|
+
text({
|
|
1721
|
+
fg: getStatusColor(statusText, theme)
|
|
1722
|
+
}, [statusText])
|
|
1723
|
+
]));
|
|
1724
|
+
}
|
|
1725
|
+
if (customSessions.length > 0) {
|
|
1726
|
+
agentRows.push(box({ width: "100%" }));
|
|
1727
|
+
const customGroups = new Map;
|
|
1728
|
+
for (const entry of customSessions) {
|
|
1729
|
+
const { sessionID, agentName, running, finished } = entry;
|
|
1730
|
+
const rawModel = mergedTreeSidebar[sessionID]?.model;
|
|
1731
|
+
const model = rawModel ? formatSidebarModelName(rawModel) : "pending";
|
|
1732
|
+
const variant = mergedTreeSidebar[sessionID]?.variant;
|
|
1733
|
+
const key = `${agentName}\x00${model}\x00${variant ?? ""}`;
|
|
1734
|
+
const group = customGroups.get(key);
|
|
1735
|
+
if (group) {
|
|
1736
|
+
group.count++;
|
|
1737
|
+
group.running = group.running || running;
|
|
1738
|
+
group.finished = group.finished || finished;
|
|
1739
|
+
} else {
|
|
1740
|
+
customGroups.set(key, {
|
|
1741
|
+
sessionID,
|
|
1742
|
+
agentName,
|
|
1743
|
+
running,
|
|
1744
|
+
finished,
|
|
1745
|
+
count: 1,
|
|
1746
|
+
model,
|
|
1747
|
+
variant
|
|
1748
|
+
});
|
|
1749
|
+
}
|
|
1750
|
+
}
|
|
1751
|
+
for (const entry of customGroups.values()) {
|
|
1752
|
+
const { sessionID, agentName, running, finished, count, variant } = entry;
|
|
1753
|
+
const elapsed = finished ? now - (mergedTreeSidebar[sessionID]?.finishedAt ?? 0) : 0;
|
|
1754
|
+
const flashDot = finished && Math.floor(elapsed / 200) % 2 === 0;
|
|
1755
|
+
const indicator = running ? spinner : flashDot ? "·" : " ";
|
|
1756
|
+
const nameStr = formatAgentName(agentName);
|
|
1757
|
+
const rawModelChild = mergedTreeSidebar[sessionID]?.model;
|
|
1758
|
+
const modelVariantLineCustom = formatSidebarModelAndVariant(rawModelChild, variant);
|
|
1759
|
+
const customStatusText = getStatusText(snapshot, sessionID);
|
|
1760
|
+
agentRows.push(box({
|
|
1761
|
+
width: "100%",
|
|
1762
|
+
flexDirection: "row",
|
|
1763
|
+
justifyContent: "space-between"
|
|
1764
|
+
}, [
|
|
1765
|
+
box({ flexDirection: "row" }, [
|
|
1766
|
+
text({ fg: theme.accent }, [`${indicator} `]),
|
|
1767
|
+
text({ fg: theme.text }, [nameStr]),
|
|
1768
|
+
text({ fg: theme.accent }, [` x${count}`])
|
|
1769
|
+
])
|
|
1770
|
+
]));
|
|
1771
|
+
agentRows.push(box({
|
|
1772
|
+
width: "100%",
|
|
1773
|
+
flexDirection: "row",
|
|
1774
|
+
justifyContent: "space-between"
|
|
1775
|
+
}, [
|
|
1776
|
+
text({ fg: theme.textMuted }, [
|
|
1777
|
+
modelVariantLineCustom.length > 0 ? ` ${modelVariantLineCustom}` : " pending"
|
|
1778
|
+
]),
|
|
1779
|
+
text({ fg: getStatusColor(customStatusText, theme) }, [
|
|
1780
|
+
customStatusText
|
|
1781
|
+
])
|
|
1782
|
+
]));
|
|
1783
|
+
}
|
|
1784
|
+
}
|
|
1785
|
+
if (agentRows.length === 0) {
|
|
1786
|
+
agentRows.push(text({ fg: theme.textMuted }, ["No active agents"]));
|
|
1787
|
+
}
|
|
1788
|
+
const orchestratingRows = buildOrchestratingRows(snapshot, now, theme);
|
|
1789
|
+
const usageRows = renderSubscriptionPanel(snapshot, theme);
|
|
1790
|
+
return box({
|
|
1791
|
+
width: "100%",
|
|
1792
|
+
flexDirection: "column",
|
|
1793
|
+
border: BORDER,
|
|
1794
|
+
borderColor: theme.borderActive,
|
|
1795
|
+
paddingTop: 0,
|
|
1796
|
+
paddingBottom: 0,
|
|
1797
|
+
paddingLeft: 0,
|
|
1798
|
+
paddingRight: 0
|
|
1799
|
+
}, [
|
|
1800
|
+
box({
|
|
1801
|
+
width: "100%",
|
|
1802
|
+
flexDirection: "row",
|
|
1803
|
+
justifyContent: "space-between"
|
|
1804
|
+
}, [
|
|
1805
|
+
text({ fg: theme.text }, ["Agents"]),
|
|
1806
|
+
text({ fg: theme.textMuted }, [`[${totalActive} active]`])
|
|
1807
|
+
]),
|
|
1808
|
+
...agentRows,
|
|
1809
|
+
...orchestratingRows.length > 0 ? [
|
|
1810
|
+
box({ width: "100%", height: 1 }),
|
|
1811
|
+
box({
|
|
1812
|
+
width: "100%",
|
|
1813
|
+
flexDirection: "column",
|
|
1814
|
+
border: BORDER,
|
|
1815
|
+
borderColor: theme.borderActive,
|
|
1816
|
+
paddingTop: 0,
|
|
1817
|
+
paddingBottom: 0,
|
|
1818
|
+
paddingLeft: 0,
|
|
1819
|
+
paddingRight: 0
|
|
1820
|
+
}, [
|
|
1821
|
+
box({
|
|
1822
|
+
width: "100%",
|
|
1823
|
+
flexDirection: "row",
|
|
1824
|
+
justifyContent: "space-between"
|
|
1825
|
+
}, [
|
|
1826
|
+
text({ fg: theme.text }, ["Orchestrating"]),
|
|
1827
|
+
text({ fg: theme.textMuted }, [
|
|
1828
|
+
`[${orchestratingRows[0]}]`
|
|
1829
|
+
])
|
|
1830
|
+
]),
|
|
1831
|
+
...orchestratingRows.slice(1)
|
|
1832
|
+
])
|
|
1833
|
+
] : [],
|
|
1834
|
+
...usageRows.length > 0 ? [
|
|
1835
|
+
box({ width: "100%", height: 1 }),
|
|
1836
|
+
box({
|
|
1837
|
+
width: "100%",
|
|
1838
|
+
flexDirection: "column",
|
|
1839
|
+
border: BORDER,
|
|
1840
|
+
borderColor: theme.borderActive,
|
|
1841
|
+
paddingTop: 0,
|
|
1842
|
+
paddingBottom: 0,
|
|
1843
|
+
paddingLeft: 0,
|
|
1844
|
+
paddingRight: 0
|
|
1845
|
+
}, [
|
|
1846
|
+
box({
|
|
1847
|
+
width: "100%",
|
|
1848
|
+
flexDirection: "row",
|
|
1849
|
+
justifyContent: "space-between"
|
|
1850
|
+
}, [text({ fg: theme.text }, ["API Usage"])]),
|
|
1851
|
+
...usageRows
|
|
1852
|
+
])
|
|
1853
|
+
] : []
|
|
1854
|
+
]);
|
|
1855
|
+
}
|
|
1856
|
+
var plugin = {
|
|
1857
|
+
id: `${PLUGIN_NAME}:tui`,
|
|
1858
|
+
tui: async (api, _options, _meta) => {
|
|
1859
|
+
const [snapshot, setSnapshot] = createSignal(readTuiSnapshot());
|
|
1860
|
+
const [tick, setTick] = createSignal(0);
|
|
1861
|
+
const dataTimer = setInterval(async () => {
|
|
1862
|
+
try {
|
|
1863
|
+
setSnapshot(await readTuiSnapshotAsync());
|
|
1864
|
+
} catch {}
|
|
1865
|
+
}, 1000);
|
|
1866
|
+
const animTimer = setInterval(() => {
|
|
1867
|
+
setTick(tick() + 1);
|
|
1868
|
+
}, 50);
|
|
1869
|
+
api.lifecycle.onDispose(() => {
|
|
1870
|
+
clearInterval(dataTimer);
|
|
1871
|
+
clearInterval(animTimer);
|
|
1872
|
+
});
|
|
1873
|
+
api.slots.register({
|
|
1874
|
+
order: 150,
|
|
1875
|
+
slots: {
|
|
1876
|
+
sidebar_content() {
|
|
1877
|
+
tick();
|
|
1878
|
+
return renderSidebar(snapshot(), api.theme.current);
|
|
1879
|
+
}
|
|
1880
|
+
}
|
|
1881
|
+
});
|
|
1882
|
+
}
|
|
1883
|
+
};
|
|
1884
|
+
var tui_default = plugin;
|
|
1885
|
+
export {
|
|
1886
|
+
getSidebarAgentNames,
|
|
1887
|
+
formatTokenAbbrevDecimal,
|
|
1888
|
+
formatTokenAbbrev,
|
|
1889
|
+
formatSidebarModelName,
|
|
1890
|
+
formatSidebarModelAndVariant,
|
|
1891
|
+
formatSessionUsageRows,
|
|
1892
|
+
formatDuration,
|
|
1893
|
+
formatAgentName,
|
|
1894
|
+
tui_default as default,
|
|
1895
|
+
aggregateOrchestrationUsage
|
|
1896
|
+
};
|