@visual-z/xeno 0.8.4 → 0.9.1
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/CHANGELOG.md +19 -0
- package/README.md +31 -746
- package/bin/xeno.exe +26 -0
- package/install.cjs +88 -0
- package/package.json +18 -48
- package/docs/app-server.md +0 -115
- package/docs/foundations.md +0 -88
- package/docs/install.md +0 -244
- package/docs/providers.md +0 -172
- package/docs/research/foundation-clients-2026-10-09.md +0 -27
- package/docs/research/foundation-editing-2026-10-09.md +0 -56
- package/docs/research/foundation-network-2026-10-09.md +0 -37
- package/docs/research/foundation-workflow-2026-10-09.md +0 -37
- package/src/apps/acp-server.ts +0 -41
- package/src/apps/agent-profiles.ts +0 -19
- package/src/apps/app-server.ts +0 -30
- package/src/apps/approve.ts +0 -49
- package/src/apps/cli.ts +0 -99
- package/src/apps/commands.ts +0 -319
- package/src/apps/completions.ts +0 -87
- package/src/apps/control-transport.ts +0 -90
- package/src/apps/export-cli.ts +0 -24
- package/src/apps/goal-flow.ts +0 -70
- package/src/apps/image-draft.ts +0 -46
- package/src/apps/input.ts +0 -312
- package/src/apps/model-catalog.ts +0 -69
- package/src/apps/preferences.ts +0 -20
- package/src/apps/project-trust.ts +0 -9
- package/src/apps/provider-connect.ts +0 -346
- package/src/apps/question-ui.ts +0 -27
- package/src/apps/render.ts +0 -181
- package/src/apps/resources.ts +0 -66
- package/src/apps/review.ts +0 -109
- package/src/apps/session-hub.ts +0 -96
- package/src/apps/session-lifecycle.ts +0 -64
- package/src/apps/shell.ts +0 -2071
- package/src/apps/startup-input.ts +0 -135
- package/src/apps/startup.ts +0 -1677
- package/src/apps/ui/app.ts +0 -1818
- package/src/apps/ui/control-model.ts +0 -43
- package/src/apps/ui/extension-host.ts +0 -194
- package/src/apps/ui/image-cache.ts +0 -121
- package/src/apps/ui/palette.ts +0 -88
- package/src/apps/ui/project.ts +0 -127
- package/src/apps/ui/standalone.ts +0 -148
- package/src/apps/ui/status-model.ts +0 -68
- package/src/apps/ui/surface.ts +0 -91
- package/src/apps/ui/theme.ts +0 -86
- package/src/apps/update-check.ts +0 -112
- package/src/apps/workflow.ts +0 -58
- package/src/core/acceptance.ts +0 -139
- package/src/core/action-defaults.ts +0 -58
- package/src/core/activity-status.ts +0 -52
- package/src/core/agent-profile.ts +0 -130
- package/src/core/attachment-input.ts +0 -17
- package/src/core/blind-checks.ts +0 -72
- package/src/core/boundary.ts +0 -174
- package/src/core/branch-receipt.ts +0 -26
- package/src/core/catalog-merge.ts +0 -19
- package/src/core/checkpoints.ts +0 -85
- package/src/core/churn.ts +0 -61
- package/src/core/command-artifacts.ts +0 -94
- package/src/core/command-contract.ts +0 -151
- package/src/core/completion-action.ts +0 -20
- package/src/core/completion-review.ts +0 -98
- package/src/core/context-detail.ts +0 -37
- package/src/core/context-policy.ts +0 -133
- package/src/core/context-window.ts +0 -54
- package/src/core/continuation.ts +0 -130
- package/src/core/control-protocol.ts +0 -69
- package/src/core/control-view.ts +0 -39
- package/src/core/cycle.ts +0 -17
- package/src/core/data-query.ts +0 -201
- package/src/core/deadline.ts +0 -40
- package/src/core/derive.ts +0 -516
- package/src/core/draft-images.ts +0 -56
- package/src/core/edit-plan.ts +0 -200
- package/src/core/environment.ts +0 -181
- package/src/core/event-validation.ts +0 -220
- package/src/core/events.ts +0 -670
- package/src/core/evidence-ladder.ts +0 -62
- package/src/core/evidence-links.ts +0 -68
- package/src/core/evidence.ts +0 -240
- package/src/core/execution.ts +0 -18
- package/src/core/exhaustive.ts +0 -18
- package/src/core/export.ts +0 -179
- package/src/core/extension-flags.ts +0 -14
- package/src/core/external-effects.ts +0 -488
- package/src/core/failure-recovery.ts +0 -173
- package/src/core/file-precondition.ts +0 -78
- package/src/core/file-references.ts +0 -35
- package/src/core/frontmatter.ts +0 -213
- package/src/core/git-status.ts +0 -29
- package/src/core/goal-view.ts +0 -64
- package/src/core/goal.ts +0 -200
- package/src/core/handoff.ts +0 -479
- package/src/core/history-query.ts +0 -139
- package/src/core/history.ts +0 -237
- package/src/core/image-limits.ts +0 -72
- package/src/core/images.ts +0 -46
- package/src/core/integration-health.ts +0 -34
- package/src/core/json-event.ts +0 -131
- package/src/core/json-pointer.ts +0 -46
- package/src/core/keymap.ts +0 -153
- package/src/core/lsp-framing.ts +0 -34
- package/src/core/mcp-config.ts +0 -68
- package/src/core/model-picker.ts +0 -64
- package/src/core/model-preferences.ts +0 -28
- package/src/core/model-replay.ts +0 -22
- package/src/core/model-scope.ts +0 -53
- package/src/core/note-guidance.ts +0 -34
- package/src/core/oauth-fields.ts +0 -44
- package/src/core/observation-view.ts +0 -152
- package/src/core/outcome.ts +0 -44
- package/src/core/overflow.ts +0 -59
- package/src/core/packages.ts +0 -47
- package/src/core/patch.ts +0 -130
- package/src/core/path-suggestions.ts +0 -59
- package/src/core/permissions.ts +0 -410
- package/src/core/process-receipt.ts +0 -105
- package/src/core/process-state.ts +0 -37
- package/src/core/program-status.ts +0 -40
- package/src/core/progress.ts +0 -150
- package/src/core/project.ts +0 -431
- package/src/core/prompt-policy.ts +0 -46
- package/src/core/prompts.ts +0 -111
- package/src/core/question.ts +0 -94
- package/src/core/queue.ts +0 -78
- package/src/core/read-bounds.ts +0 -24
- package/src/core/read-observation.ts +0 -106
- package/src/core/recovery-sources.ts +0 -74
- package/src/core/regex-dialect.ts +0 -25
- package/src/core/request-budget.ts +0 -23
- package/src/core/request-progress.ts +0 -87
- package/src/core/request-state.ts +0 -273
- package/src/core/resource-selection.ts +0 -36
- package/src/core/response-recovery.ts +0 -59
- package/src/core/resume.ts +0 -59
- package/src/core/review.ts +0 -68
- package/src/core/ripgrep-release.ts +0 -64
- package/src/core/runtime-target.ts +0 -94
- package/src/core/scheduling.ts +0 -8
- package/src/core/search-observation.ts +0 -97
- package/src/core/secrets.ts +0 -188
- package/src/core/session-archive.ts +0 -4
- package/src/core/session-browser.ts +0 -82
- package/src/core/session-entry.ts +0 -61
- package/src/core/session-exit.ts +0 -42
- package/src/core/session-format.ts +0 -169
- package/src/core/session-position.ts +0 -26
- package/src/core/session-tree.ts +0 -147
- package/src/core/settings.ts +0 -192
- package/src/core/skills.ts +0 -165
- package/src/core/sql-query.ts +0 -211
- package/src/core/stall.ts +0 -128
- package/src/core/stop-report.ts +0 -68
- package/src/core/text-normalize.ts +0 -81
- package/src/core/theme.ts +0 -166
- package/src/core/thinking.ts +0 -26
- package/src/core/tokens.ts +0 -151
- package/src/core/tool-contract.ts +0 -153
- package/src/core/tool-discovery.ts +0 -87
- package/src/core/tool-failure.ts +0 -81
- package/src/core/tool-input.ts +0 -44
- package/src/core/transcript.ts +0 -108
- package/src/core/tui-mode.ts +0 -29
- package/src/core/update-check.ts +0 -157
- package/src/core/version.ts +0 -55
- package/src/core/web-content.ts +0 -80
- package/src/core/work-state.ts +0 -154
- package/src/core/workflow-mode.ts +0 -34
- package/src/core/working-set.ts +0 -119
- package/src/core/yaml.ts +0 -282
- package/src/runtime/acp-agent.ts +0 -289
- package/src/runtime/agent-profiles.ts +0 -21
- package/src/runtime/auth.ts +0 -367
- package/src/runtime/blind-check.ts +0 -36
- package/src/runtime/branch-jobs.ts +0 -36
- package/src/runtime/catalog.ts +0 -285
- package/src/runtime/clipboard.ts +0 -125
- package/src/runtime/command-artifacts.ts +0 -64
- package/src/runtime/connection-reuse.ts +0 -40
- package/src/runtime/control-client.ts +0 -149
- package/src/runtime/control-connection.ts +0 -139
- package/src/runtime/counter-check.ts +0 -74
- package/src/runtime/environment-tools.ts +0 -60
- package/src/runtime/evidence-notes.ts +0 -35
- package/src/runtime/exec-sessions.ts +0 -456
- package/src/runtime/execution-control.ts +0 -40
- package/src/runtime/extension-loader.ts +0 -37
- package/src/runtime/extension-ui.ts +0 -47
- package/src/runtime/extensions.ts +0 -370
- package/src/runtime/formatter-tool.ts +0 -38
- package/src/runtime/formatter.ts +0 -50
- package/src/runtime/foundation.ts +0 -116
- package/src/runtime/history-cache.ts +0 -57
- package/src/runtime/history-tools.ts +0 -168
- package/src/runtime/images.ts +0 -80
- package/src/runtime/integrations.ts +0 -216
- package/src/runtime/loop.ts +0 -3108
- package/src/runtime/lsp-client.ts +0 -201
- package/src/runtime/mcp-client.ts +0 -264
- package/src/runtime/mcp-oauth.ts +0 -168
- package/src/runtime/mcp-schema.ts +0 -51
- package/src/runtime/model-config.ts +0 -232
- package/src/runtime/model-data.LICENSE +0 -27
- package/src/runtime/model-data.ts +0 -21
- package/src/runtime/model-discovery.ts +0 -208
- package/src/runtime/model-replay.ts +0 -10
- package/src/runtime/model-scope.ts +0 -20
- package/src/runtime/model-sources.ts +0 -151
- package/src/runtime/model.ts +0 -686
- package/src/runtime/models-fetch.ts +0 -57
- package/src/runtime/oauth-device.ts +0 -86
- package/src/runtime/oauth.ts +0 -853
- package/src/runtime/open-image.ts +0 -42
- package/src/runtime/openai-responses.ts +0 -304
- package/src/runtime/openai.ts +0 -697
- package/src/runtime/output-schema.ts +0 -26
- package/src/runtime/package-manager.ts +0 -125
- package/src/runtime/patch-tool.ts +0 -38
- package/src/runtime/permission-resources.ts +0 -18
- package/src/runtime/pi-provider.ts +0 -423
- package/src/runtime/program-status.ts +0 -38
- package/src/runtime/progress.ts +0 -26
- package/src/runtime/provider-access.ts +0 -21
- package/src/runtime/provider-connect.ts +0 -109
- package/src/runtime/provider-presets.ts +0 -98
- package/src/runtime/provider-protocols.ts +0 -29
- package/src/runtime/provider-request.ts +0 -87
- package/src/runtime/provider-tool-policy.ts +0 -33
- package/src/runtime/provider.ts +0 -580
- package/src/runtime/question-tool.ts +0 -62
- package/src/runtime/retry.ts +0 -447
- package/src/runtime/review-tool.ts +0 -48
- package/src/runtime/rich-text.ts +0 -36
- package/src/runtime/runtime-target.ts +0 -149
- package/src/runtime/secrets.ts +0 -79
- package/src/runtime/self-update.ts +0 -74
- package/src/runtime/session-control.ts +0 -265
- package/src/runtime/session-hub.ts +0 -85
- package/src/runtime/stream-idle.ts +0 -80
- package/src/runtime/tool-broker.ts +0 -137
- package/src/runtime/tool-discovery.ts +0 -58
- package/src/runtime/tool-queue.ts +0 -77
- package/src/runtime/tools.ts +0 -1820
- package/src/runtime/turn-effects.ts +0 -31
- package/src/runtime/update-check.ts +0 -48
- package/src/runtime/usage.ts +0 -253
- package/src/runtime/version.ts +0 -12
- package/src/runtime/web-http.ts +0 -56
- package/src/runtime/web-tools.ts +0 -88
- package/src/runtime/working-notes.ts +0 -60
- package/src/runtime/workspace-lifecycle.ts +0 -141
- package/src/store/acceptance.ts +0 -15
- package/src/store/agent-profile-files.ts +0 -13
- package/src/store/atomic.ts +0 -31
- package/src/store/auth-store.ts +0 -50
- package/src/store/blind-checks.ts +0 -32
- package/src/store/blobs.ts +0 -143
- package/src/store/checkpoint-history.ts +0 -32
- package/src/store/command-artifacts.ts +0 -96
- package/src/store/command.ts +0 -111
- package/src/store/config.ts +0 -69
- package/src/store/context-files.ts +0 -436
- package/src/store/file-checkpoints.ts +0 -108
- package/src/store/file-index.ts +0 -174
- package/src/store/foundation-config.ts +0 -24
- package/src/store/git-checkpoints.ts +0 -67
- package/src/store/git-status.ts +0 -15
- package/src/store/history-search.ts +0 -19
- package/src/store/log.ts +0 -725
- package/src/store/lsp-document.ts +0 -30
- package/src/store/mcp-credentials.ts +0 -85
- package/src/store/models.ts +0 -60
- package/src/store/observations.ts +0 -224
- package/src/store/packages.ts +0 -98
- package/src/store/patch.ts +0 -92
- package/src/store/permission-paths.ts +0 -34
- package/src/store/process-receipts.ts +0 -34
- package/src/store/query-source.ts +0 -49
- package/src/store/recovery.ts +0 -44
- package/src/store/replay.ts +0 -76
- package/src/store/resource-index.ts +0 -170
- package/src/store/resources.ts +0 -285
- package/src/store/restore.ts +0 -110
- package/src/store/review.ts +0 -109
- package/src/store/runtime-target.ts +0 -64
- package/src/store/search.ts +0 -728
- package/src/store/secrets.ts +0 -44
- package/src/store/serial.ts +0 -13
- package/src/store/session-archive.ts +0 -165
- package/src/store/session-browser.ts +0 -68
- package/src/store/session-position.ts +0 -27
- package/src/store/sessions.ts +0 -525
- package/src/store/source-cancellation.ts +0 -6
- package/src/store/tool-binaries.ts +0 -90
- package/src/store/trust.ts +0 -82
- package/src/store/update-cache.ts +0 -36
- package/src/store/user-settings.ts +0 -30
- package/src/store/workspace-clone.ts +0 -42
- package/src/store/workspace-observation.ts +0 -113
- package/src/store/workspace.ts +0 -115
- package/src/store/worktree.ts +0 -278
- package/src/tui/ansi-spans.ts +0 -126
- package/src/tui/diff.ts +0 -160
- package/src/tui/exhaustive.ts +0 -14
- package/src/tui/fuzzy.ts +0 -75
- package/src/tui/style.ts +0 -208
- package/src/tui/ui/atomic-draft.ts +0 -121
- package/src/tui/ui/branch-panel.ts +0 -47
- package/src/tui/ui/canvas.ts +0 -96
- package/src/tui/ui/colors.ts +0 -70
- package/src/tui/ui/composer.ts +0 -199
- package/src/tui/ui/control-panel.ts +0 -25
- package/src/tui/ui/control-sidebar.ts +0 -49
- package/src/tui/ui/dialog.ts +0 -398
- package/src/tui/ui/draft.ts +0 -23
- package/src/tui/ui/duration.ts +0 -16
- package/src/tui/ui/edge.ts +0 -186
- package/src/tui/ui/image-size.ts +0 -44
- package/src/tui/ui/ink.ts +0 -210
- package/src/tui/ui/items.ts +0 -42
- package/src/tui/ui/keys.ts +0 -40
- package/src/tui/ui/layout.ts +0 -74
- package/src/tui/ui/list-model.ts +0 -86
- package/src/tui/ui/list-view.ts +0 -172
- package/src/tui/ui/mascot.ts +0 -67
- package/src/tui/ui/messages.ts +0 -518
- package/src/tui/ui/motion.ts +0 -28
- package/src/tui/ui/paths.ts +0 -10
- package/src/tui/ui/pictures.ts +0 -108
- package/src/tui/ui/prompt-history.ts +0 -60
- package/src/tui/ui/screen.ts +0 -579
- package/src/tui/ui/status.ts +0 -403
- package/src/tui/ui/styled.ts +0 -85
- package/src/tui/ui/tool-card.ts +0 -150
- package/src/tui/width.ts +0 -567
- package/src/vendor/pi-rich/LICENSE.upstream +0 -21
- package/src/vendor/pi-rich/UPSTREAM.json +0 -17
- package/src/vendor/pi-rich/clipboard-command.ts +0 -44
- package/src/vendor/pi-rich/exif-orientation.ts +0 -182
- package/src/vendor/pi-rich/html.ts +0 -51
- package/src/vendor/pi-rich/image-convert.ts +0 -49
- package/src/vendor/pi-rich/image-resize-core.ts +0 -164
- package/src/vendor/pi-rich/image-resize-worker.ts +0 -42
- package/src/vendor/pi-rich/image-resize.ts +0 -123
- package/src/vendor/pi-rich/mime.ts +0 -116
- package/src/vendor/pi-rich/photon.ts +0 -139
- package/src/vendor/pi-rich/syntax-highlight.ts +0 -214
package/src/tui/ui/status.ts
DELETED
|
@@ -1,403 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Session status as rows of styled segments (docs/tasks/refactor-tui-dock-layout.md). Pure.
|
|
3
|
-
*
|
|
4
|
-
* The app gathers the numbers (log projection, handoff preview, usage
|
|
5
|
-
* totals); these functions only decide what fits where. Which fields a tier
|
|
6
|
-
* shows is layout.ts's decision; this module formats them for the sidebar,
|
|
7
|
-
* the top bar and the input box edge.
|
|
8
|
-
*
|
|
9
|
-
* Field order follows what users ask for most (research 2026-10-06: context
|
|
10
|
-
* share, model and effort, location, cost, files, tasks, background work),
|
|
11
|
-
* then xeno's own: the handoff the next segment would start from. Every block
|
|
12
|
-
* appears only when it has something to say; complaints about status that is
|
|
13
|
-
* "a paragraph" were as common as requests for more of it.
|
|
14
|
-
*/
|
|
15
|
-
import { clipSegs, clipText, segsWidth, splitLine, type Label, type Seg } from "./edge.ts";
|
|
16
|
-
import { displayWidth } from "../width.ts";
|
|
17
|
-
import { formatElapsed } from "./duration.ts";
|
|
18
|
-
|
|
19
|
-
export interface ContextStatus {
|
|
20
|
-
readonly used: number;
|
|
21
|
-
readonly window?: number;
|
|
22
|
-
/** Where the next segment cut happens, in the same units; null when the policy never cuts. */
|
|
23
|
-
readonly trigger: number | null;
|
|
24
|
-
readonly estimated: boolean;
|
|
25
|
-
readonly segment: number;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export interface FileStatus { readonly path: string; readonly edited: boolean; readonly added: number; readonly removed: number }
|
|
29
|
-
|
|
30
|
-
export interface StatusModel {
|
|
31
|
-
readonly version: string;
|
|
32
|
-
/** `/name`, else the first prompt; null for a session with neither. */
|
|
33
|
-
readonly title: string | null;
|
|
34
|
-
/**
|
|
35
|
-
* An explicit /goal, prepared by apps from the log (core/goal-view.ts). The
|
|
36
|
-
* block exists only while a goal is set, and for one message after it is met.
|
|
37
|
-
*/
|
|
38
|
-
readonly goal?: GoalStatusView;
|
|
39
|
-
/** Without a goal: the latest check after this request's last edit, when it changed files. */
|
|
40
|
-
readonly checks?: { readonly status: "unchecked" | "pass" | "fail" | "stale" | "partial"; readonly command?: string; readonly exit?: number };
|
|
41
|
-
readonly workspace: string;
|
|
42
|
-
readonly workspaceShort: string;
|
|
43
|
-
readonly branch: string | null;
|
|
44
|
-
readonly mode?: { readonly yolo: boolean; readonly workflow: "build" | "plan" };
|
|
45
|
-
readonly provider?: { readonly name: string; readonly api: string; readonly host: string | null; readonly role: string | null; readonly ready: boolean };
|
|
46
|
-
readonly activity?: { readonly state: string; readonly label: string; readonly detail: string; readonly queued: number };
|
|
47
|
-
readonly git?: { readonly staged: number; readonly changed: number; readonly untracked: number; readonly conflicted: number; readonly ahead: number; readonly behind: number } | null;
|
|
48
|
-
readonly context?: ContextStatus;
|
|
49
|
-
/** `speed`: output tokens per second over this turn's model requests (runtime/usage.ts generationSpeed). */
|
|
50
|
-
readonly usage?: { readonly input: number; readonly output: number; readonly cacheRate: number; readonly cost: string | null; readonly speed?: number };
|
|
51
|
-
readonly tools: { readonly running: number; readonly done: number; readonly failed: number };
|
|
52
|
-
readonly handoff?: { readonly verified: readonly string[]; readonly inFlight: readonly string[]; readonly unverified: readonly string[] };
|
|
53
|
-
readonly tasks?: readonly { readonly what: string; readonly status: "active" | "blocked" }[];
|
|
54
|
-
readonly files?: readonly FileStatus[];
|
|
55
|
-
readonly background?: readonly string[];
|
|
56
|
-
readonly resources?: { readonly contextFiles: readonly string[]; readonly skills: number; readonly extensions: number };
|
|
57
|
-
readonly extension?: string;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
export interface GoalStatusView {
|
|
61
|
-
readonly phase: "drafting" | "working" | "needs_you" | "met" | "stopped";
|
|
62
|
-
readonly objective: string;
|
|
63
|
-
readonly elapsedMs: number;
|
|
64
|
-
readonly checks: readonly { readonly id: string; readonly kind: "command" | "human" | "fresh_check"; readonly status: string; readonly guard?: true; readonly exit?: number }[];
|
|
65
|
-
/** S.1: one glyph per rung (ran, tests the change, independent, human), prepared by apps. */
|
|
66
|
-
readonly ladder?: { readonly glyphs: string; readonly summary: string };
|
|
67
|
-
/** Model requests since the goal was set (from the log) and output tokens this process saw. */
|
|
68
|
-
readonly requests: number;
|
|
69
|
-
readonly tokens?: number;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
export type Line = readonly Seg[];
|
|
73
|
-
|
|
74
|
-
/** 1234 → "1.2k", 1234567 → "1.2m". The same compact form everywhere a token count appears. */
|
|
75
|
-
export function compact(value: number): string {
|
|
76
|
-
return value >= 1_000_000 ? `${(value / 1_000_000).toFixed(1)}m` : value >= 1000 ? `${(value / 1000).toFixed(1)}k` : String(Math.round(value));
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* Share of the budget before the next segment cut. The cut, not the model
|
|
81
|
-
* window, is when this segment's context is replaced by a handoff, so it is
|
|
82
|
-
* the number that says how much room is left (D1/D8).
|
|
83
|
-
*/
|
|
84
|
-
export function contextShare(context: ContextStatus): number | undefined {
|
|
85
|
-
const limit = context.trigger ?? context.window;
|
|
86
|
-
return limit ? Math.max(0, Math.min(1, context.used / limit)) : undefined;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/** Above this share the context figure turns to the warning colour. Assumed, matching the old gauge. */
|
|
90
|
-
const CONTEXT_WARNING = 0.8;
|
|
91
|
-
|
|
92
|
-
function shareSeg(context: ContextStatus): Seg | undefined {
|
|
93
|
-
const share = contextShare(context);
|
|
94
|
-
if (share === undefined) return undefined;
|
|
95
|
-
return { text: `${context.estimated ? "~" : ""}${Math.round(share * 100)}%`, role: share >= CONTEXT_WARNING ? "warning" : "text" };
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/** The input box's bottom-right label names the measured budget: `seg ~12%` or `model ~12%`. */
|
|
99
|
-
export function contextLabel(model: StatusModel): Label | undefined {
|
|
100
|
-
const share = model.context && shareSeg(model.context);
|
|
101
|
-
return share ? { segs: [{ text: model.context?.trigger === null ? "model " : "seg ", role: "muted" }, share], priority: 3 } : undefined;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
export function modeLabels(model: StatusModel): Label[] {
|
|
105
|
-
if (!model.mode) return [];
|
|
106
|
-
return [
|
|
107
|
-
{ segs: [{ text: model.mode.yolo ? "YOLO" : "Approval", role: model.mode.yolo ? "warning" : "muted", bold: model.mode.yolo }], priority: 0 },
|
|
108
|
-
{ segs: [{ text: model.mode.workflow === "plan" ? "Plan" : "Build", role: "accent" }], priority: 0 },
|
|
109
|
-
];
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
export function terminalTitle(model: StatusModel): string {
|
|
113
|
-
return ["xeno", model.branch, model.title ? clipText(firstLine(model.title), 48) : null].filter(Boolean).join(" · ");
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
function firstLine(text: string): string { return text.split("\n").find(line => line.trim())?.trim() ?? ""; }
|
|
117
|
-
|
|
118
|
-
// ------------------------------------------------------------------- top bar
|
|
119
|
-
|
|
120
|
-
/**
|
|
121
|
-
* `xeno title ………… ~/P/w/xeno-tui ⎇ branch ✓3 …1 ⧗ 1 $0.42`
|
|
122
|
-
* Labels read in display order; priority decides what drops first when the
|
|
123
|
-
* row is narrow: location goes before counts, as OpenCode #570 asked.
|
|
124
|
-
*/
|
|
125
|
-
export function topbarRow(model: StatusModel, width: number): Seg[] {
|
|
126
|
-
if (model.provider) {
|
|
127
|
-
const provider = model.provider;
|
|
128
|
-
return splitLine(width, [
|
|
129
|
-
{ segs: [{ text: "xeno", role: "text", bold: true }], priority: 0 },
|
|
130
|
-
{ segs: [{ text: `${provider.ready ? "" : "! "}${provider.name}`, role: provider.ready ? "text" : "warning" }], priority: 0, min: 6 },
|
|
131
|
-
{ segs: [{ text: provider.api, role: "muted" }], priority: 2, min: 8 },
|
|
132
|
-
...(provider.host ? [{ segs: [{ text: provider.host, role: "muted" as const }], priority: 3, min: 8 }] : []),
|
|
133
|
-
...(provider.role ? [{ segs: [{ text: `agent ${provider.role}`, role: "accent" as const }], priority: 1, min: 8 }] : []),
|
|
134
|
-
], model.title ? [{ segs: [{ text: firstLine(model.title), role: "muted" }], priority: 4, min: 12 }] : []);
|
|
135
|
-
}
|
|
136
|
-
const left: Label[] = [
|
|
137
|
-
{ segs: [{ text: "xeno", role: "text", bold: true }], priority: 0 },
|
|
138
|
-
// Never dropped, but gives up width after cost and background counts, before the handoff counts go.
|
|
139
|
-
{ segs: [{ text: model.title ? firstLine(model.title) : "New session", role: model.title ? "text" : "muted" }], priority: 0, min: 12, shrinkPriority: 1.5 },
|
|
140
|
-
];
|
|
141
|
-
const right: Label[] = [{ segs: [{ text: model.workspaceShort, role: "muted" }], priority: 6 }];
|
|
142
|
-
if (model.branch) right.push({ segs: [{ text: "⎇ ", role: "muted" }, { text: model.branch, role: "muted" }], priority: 5, min: 8, shrink: 1 });
|
|
143
|
-
const counts = handoffCounts(model);
|
|
144
|
-
if (counts.length) right.push({ segs: counts, priority: 1 });
|
|
145
|
-
if (model.background?.length) right.push({ segs: [{ text: "⧗ ", role: "accent" }, { text: String(model.background.length), role: "text" }], priority: 2 });
|
|
146
|
-
if (model.extension) right.push({ segs: [{ text: model.extension, role: "muted" }], priority: 4, min: 10 });
|
|
147
|
-
if (model.usage?.cost) right.push({ segs: [{ text: model.usage.cost, role: "muted" }], priority: 3 });
|
|
148
|
-
return splitLine(width, left, right);
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
function handoffCounts(model: StatusModel): Seg[] {
|
|
152
|
-
const h = model.handoff;
|
|
153
|
-
if (!h) return [];
|
|
154
|
-
const out: Seg[] = [];
|
|
155
|
-
const add = (glyph: string, role: Seg["role"], count: number) => { if (count) out.push(...(out.length ? [{ text: " ", role: "muted" as const }] : []), { text: glyph, role }, { text: String(count), role: "text" }); };
|
|
156
|
-
add("✓", "success", h.verified.length);
|
|
157
|
-
add("…", "accent", h.inFlight.length);
|
|
158
|
-
add("?", "warning", h.unverified.length);
|
|
159
|
-
return out;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
// ------------------------------------------------------------------- sidebar
|
|
163
|
-
|
|
164
|
-
interface Block {
|
|
165
|
-
/** Rows that are always shown together (a header and its fixed facts). */
|
|
166
|
-
readonly head: readonly Line[];
|
|
167
|
-
/** Rows that may be cut, with a "+N more" row. */
|
|
168
|
-
readonly items: readonly Line[];
|
|
169
|
-
/** A list block with no items is not shown at all. */
|
|
170
|
-
readonly list: boolean;
|
|
171
|
-
/** Rows shown after the items, kept with the head (the goal's ladder and spend). */
|
|
172
|
-
readonly tail?: readonly Line[];
|
|
173
|
-
/** A list shown even without items: a goal being drafted has no checks yet. */
|
|
174
|
-
readonly keep?: boolean;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
/**
|
|
178
|
-
* The sidebar, `height` rows of at most `width` cells. Blocks are placed in
|
|
179
|
-
* priority order; a list gets at least two rows (or all of its items), then
|
|
180
|
-
* leftover rows go to the lists in the same order. A cut list ends in
|
|
181
|
-
* "+N more" so nothing disappears silently.
|
|
182
|
-
*/
|
|
183
|
-
export function sidebarLines(model: StatusModel, width: number, height: number): Line[] {
|
|
184
|
-
const w = Math.max(1, Math.floor(width)), h = Math.max(0, Math.floor(height));
|
|
185
|
-
const blocks = sidebarBlocks(model, w).filter(block => !block.list || block.items.length > 0 || block.keep);
|
|
186
|
-
const plan: { block: Block; rows: number }[] = [];
|
|
187
|
-
let used = 0;
|
|
188
|
-
for (const block of blocks) {
|
|
189
|
-
const minimum = block.list ? Math.min(block.items.length, 2) : 0;
|
|
190
|
-
const need = (plan.length ? 1 : 0) + block.head.length + minimum + (block.tail?.length ?? 0);
|
|
191
|
-
if (used + need > h) continue;
|
|
192
|
-
used += need;
|
|
193
|
-
plan.push({ block, rows: minimum });
|
|
194
|
-
}
|
|
195
|
-
for (const entry of plan) {
|
|
196
|
-
if (!entry.block.list) continue;
|
|
197
|
-
const extra = Math.min(entry.block.items.length - entry.rows, h - used);
|
|
198
|
-
if (extra > 0) { entry.rows += extra; used += extra; }
|
|
199
|
-
}
|
|
200
|
-
const out: Line[] = [];
|
|
201
|
-
plan.forEach((entry, index) => {
|
|
202
|
-
if (index) out.push([]);
|
|
203
|
-
out.push(...entry.block.head);
|
|
204
|
-
const items = entry.block.items;
|
|
205
|
-
if (entry.rows >= items.length) out.push(...items);
|
|
206
|
-
else if (entry.rows > 0) {
|
|
207
|
-
out.push(...items.slice(0, entry.rows - 1));
|
|
208
|
-
out.push([{ text: `+${items.length - entry.rows + 1} more`, role: "muted" }]);
|
|
209
|
-
}
|
|
210
|
-
out.push(...entry.block.tail ?? []);
|
|
211
|
-
});
|
|
212
|
-
return out.slice(0, h).map(line => clipSegs(line, w));
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
function sidebarBlocks(model: StatusModel, w: number): Block[] {
|
|
216
|
-
const blocks: Block[] = [];
|
|
217
|
-
const row = (left: readonly Seg[], right: readonly Seg[] = []): Line => right.length ? splitLine(w, [{ segs: left, priority: 0 }], [{ segs: right, priority: 1 }], 1) : left;
|
|
218
|
-
const title = model.title ? wrap(firstLine(model.title), w, 2) : ["New session"];
|
|
219
|
-
blocks.push({ list: false, items: [], head: [
|
|
220
|
-
row([{ text: "xeno", role: "text", bold: true }], [{ text: model.version, role: "muted" }]),
|
|
221
|
-
[],
|
|
222
|
-
...title.map(text => [{ text, role: model.title ? "text" : "muted", bold: Boolean(model.title) }] as Line),
|
|
223
|
-
[{ text: clipText(model.workspace, w), role: "muted" }],
|
|
224
|
-
...(model.branch ? [[{ text: "⎇ ", role: "muted" }, { text: model.branch, role: "muted" }] as Line] : []),
|
|
225
|
-
...(model.git ? [[{ text: `${model.git.staged} staged · ${model.git.changed} changed · ${model.git.untracked} new`, role: "muted" }] as Line,
|
|
226
|
-
...(model.git.conflicted ? [[{ text: `${model.git.conflicted} conflicts`, role: "warning" }] as Line] : []),
|
|
227
|
-
...(model.git.ahead || model.git.behind ? [[{ text: `↑${model.git.ahead} ↓${model.git.behind}`, role: "muted" }] as Line] : [])] : model.git === null ? [[{ text: "Git unavailable", role: "muted" }] as Line] : []),
|
|
228
|
-
] });
|
|
229
|
-
const goal = model.goal;
|
|
230
|
-
if (goal) blocks.push(goalBlock(goal, w, row));
|
|
231
|
-
else if (model.checks) blocks.push({ list: false, items: [], head: [checksLine(model.checks, w)] });
|
|
232
|
-
const context = model.context;
|
|
233
|
-
if (context) {
|
|
234
|
-
const share = contextShare(context), est = context.estimated ? "~" : "";
|
|
235
|
-
const filled = share === undefined ? 0 : Math.max(share > 0 ? 1 : 0, Math.round(share * w));
|
|
236
|
-
const tone = share !== undefined && share >= CONTEXT_WARNING ? "warning" : "accent";
|
|
237
|
-
const shareText = shareSeg(context);
|
|
238
|
-
// Two budgets, each named: the segment cut (what the bar shows) and the model window. Both shares
|
|
239
|
-
// ride on the heading, both denominators on one row below the bar.
|
|
240
|
-
const modelShare = context.window && context.trigger !== null ? `${est}${Math.round(context.used / context.window * 100)}%` : undefined;
|
|
241
|
-
const segment = context.trigger !== null ? `seg ${est}${compact(context.used)} / ${est}${compact(context.trigger)}` : `${est}${compact(context.used)} · no cut`;
|
|
242
|
-
blocks.push({ list: false, items: [], head: [
|
|
243
|
-
row([{ text: "Context", role: "text", bold: true }], [
|
|
244
|
-
...(shareText ? [{ text: context.trigger === null ? "model " : "seg ", role: "muted" as const }, shareText] : []),
|
|
245
|
-
...(modelShare ? [{ text: ` · model ${modelShare}`, role: "muted" as const }] : [])]),
|
|
246
|
-
...(share === undefined ? [] : [[{ text: "━".repeat(filled), role: tone }, { text: "─".repeat(Math.max(0, w - filled)), role: "line" }] as Line]),
|
|
247
|
-
row([{ text: segment, role: "muted" }], [{ text: context.window ? `model ${compact(context.window)}` : "window unknown", role: "muted" }]),
|
|
248
|
-
] });
|
|
249
|
-
}
|
|
250
|
-
const usage = model.usage, tools = model.tools;
|
|
251
|
-
const toolCount = tools.done + tools.failed + tools.running;
|
|
252
|
-
if (usage || toolCount) {
|
|
253
|
-
const figures = usage ? [`↑${compact(usage.input)} ↓${compact(usage.output)}`, `cache ${Math.round(usage.cacheRate * 100)}%`,
|
|
254
|
-
...(usage.speed !== undefined ? [speedText(usage.speed)] : [])].join(" · ") : "";
|
|
255
|
-
// Heading with the cost; one row of figures below it. Together they need about 49 columns, more than the 34-42 the sidebar has.
|
|
256
|
-
blocks.push({ list: false, items: [], head: [
|
|
257
|
-
row([{ text: "Usage", role: "text", bold: true }], usage?.cost ? [{ text: usage.cost, role: "text" }] : []),
|
|
258
|
-
...(figures ? [[{ text: figures, role: "muted" }] as Line] : []),
|
|
259
|
-
...(tools.failed || tools.running ? [[
|
|
260
|
-
{ text: `${tools.done} done`, role: "muted" },
|
|
261
|
-
...(tools.failed ? [{ text: " · ", role: "muted" as const }, { text: `${tools.failed} failed`, role: "error" as const }] : []),
|
|
262
|
-
...(tools.running ? [{ text: " · ", role: "muted" as const }, { text: `${tools.running} running`, role: "accent" as const }] : []),
|
|
263
|
-
] as Line] : []),
|
|
264
|
-
] });
|
|
265
|
-
}
|
|
266
|
-
const handoff = model.handoff;
|
|
267
|
-
if (handoff) {
|
|
268
|
-
const items: Line[] = [
|
|
269
|
-
...handoff.verified.map(what => [{ text: "✓ ", role: "success" }, { text: what, role: "text" }] as Line),
|
|
270
|
-
...handoff.inFlight.map(what => [{ text: "… ", role: "accent" }, { text: what, role: "text" }] as Line),
|
|
271
|
-
...handoff.unverified.map(what => [{ text: "? ", role: "warning" }, { text: what, role: "text" }] as Line),
|
|
272
|
-
];
|
|
273
|
-
blocks.push({ list: true, items, head: [row([{ text: "Handoff", role: "text", bold: true }], [{ text: "if cut now", role: "muted" }])] });
|
|
274
|
-
}
|
|
275
|
-
const tasks = model.tasks ?? [];
|
|
276
|
-
blocks.push({ list: true, head: [row([{ text: "Tasks", role: "text", bold: true }], [{ text: `${tasks.length} open`, role: "muted" }])],
|
|
277
|
-
items: tasks.map(task => [{ text: task.status === "blocked" ? "! " : "◐ ", role: task.status === "blocked" ? "warning" : "accent" }, { text: task.what, role: "text" }] as Line) });
|
|
278
|
-
const files = model.files ?? [];
|
|
279
|
-
blocks.push({ list: true, head: [row([{ text: "Agent files", role: "text", bold: true }], [{ text: String(files.length), role: "muted" }])],
|
|
280
|
-
items: files.map(file => fileLine(file, w)) });
|
|
281
|
-
const background = model.background ?? [];
|
|
282
|
-
blocks.push({ list: true, head: [row([{ text: "Background", role: "text", bold: true }], [{ text: String(background.length), role: "muted" }])],
|
|
283
|
-
items: background.map(what => [{ text: "⧗ ", role: "accent" }, { text: what, role: "text" }] as Line) });
|
|
284
|
-
const resources = model.resources;
|
|
285
|
-
if (resources && (resources.contextFiles.length || resources.skills || resources.extensions)) {
|
|
286
|
-
const parts = [
|
|
287
|
-
...(resources.contextFiles.length ? [resources.contextFiles.length > 1 ? `${resources.contextFiles[0]} ×${resources.contextFiles.length}` : resources.contextFiles[0]!] : []),
|
|
288
|
-
...(resources.skills ? [`${resources.skills} skill${resources.skills === 1 ? "" : "s"}`] : []),
|
|
289
|
-
...(resources.extensions ? [`${resources.extensions} extension${resources.extensions === 1 ? "" : "s"}`] : []),
|
|
290
|
-
];
|
|
291
|
-
blocks.push({ list: false, items: [], head: [[{ text: "Loaded", role: "text", bold: true }], [{ text: parts.join(" · "), role: "muted" }]] });
|
|
292
|
-
}
|
|
293
|
-
if (model.extension) blocks.push({ list: false, items: [], head: wrap(model.extension, w, 3).map(text => [{ text, role: "muted" }] as Line) });
|
|
294
|
-
return blocks;
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
/** `42 tok/s`; under 10 one decimal, so a slow upstream does not read as zero. */
|
|
298
|
-
export function speedText(speed: number): string {
|
|
299
|
-
return `${speed < 10 ? speed.toFixed(1) : Math.round(speed)} tok/s`;
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
const PHASE: Record<GoalStatusView["phase"], { label: string; role: Seg["role"] }> = {
|
|
303
|
-
drafting: { label: "drafting", role: "accent" }, working: { label: "working", role: "accent" },
|
|
304
|
-
needs_you: { label: "needs you", role: "warning" }, met: { label: "met", role: "success" }, stopped: { label: "stopped", role: "warning" },
|
|
305
|
-
};
|
|
306
|
-
|
|
307
|
-
/**
|
|
308
|
-
* `Goal · needs you 4m 12s`, the objective, one row per accepted check with
|
|
309
|
-
* what it needs, the evidence ladder and what the goal has spent. A met goal
|
|
310
|
-
* is one summary row. The sidebar may cut the checks; a cut ends in "+N more".
|
|
311
|
-
*/
|
|
312
|
-
function goalBlock(goal: GoalStatusView, w: number, row: (left: readonly Seg[], right?: readonly Seg[]) => Line): Block {
|
|
313
|
-
const phase = PHASE[goal.phase];
|
|
314
|
-
const passed = goal.checks.filter(check => check.status === "pass" || check.status === "accepted").length;
|
|
315
|
-
const elapsed = goal.phase === "drafting" ? [] : [{ text: formatElapsed(goal.elapsedMs), role: "muted" as const }];
|
|
316
|
-
const title = [{ text: "Goal", role: "text" as const, bold: true }, { text: ` · ${phase.label}`, role: phase.role }];
|
|
317
|
-
if (goal.phase === "met") return { list: false, items: [], head: [row(title, [{ text: `${passed}/${goal.checks.length} · `, role: "muted" }, ...elapsed])] };
|
|
318
|
-
const items = goal.checks.map(check => {
|
|
319
|
-
const done = check.status === "pass" || check.status === "accepted";
|
|
320
|
-
const glyph: Seg = done ? { text: "✓ ", role: "success" } : check.status === "fail" ? { text: "✗ ", role: "error" } : { text: "○ ", role: "muted" };
|
|
321
|
-
const need = check.kind === "human" && !done ? `/accept ${check.id}` : check.status === "fail" && check.exit !== undefined ? `exit ${check.exit}`
|
|
322
|
-
: check.status === "stale" ? "rerun" : check.guard ? "guard" : "";
|
|
323
|
-
return row([glyph, { text: check.id, role: "text" }], need ? [{ text: need, role: check.kind === "human" && !done ? "warning" : "muted" }] : []);
|
|
324
|
-
});
|
|
325
|
-
const spent = [`${goal.requests} ${goal.requests === 1 ? "request" : "requests"}`, ...(goal.tokens ? [`${compact(goal.tokens)} tokens this run`] : [])].join(" · ");
|
|
326
|
-
return { list: true, keep: true, head: [row(title, elapsed), ...wrap(firstLine(goal.objective), w, 2).map(text => [{ text, role: "text" }] as Line)],
|
|
327
|
-
items, ...(goal.phase === "drafting" ? {} : { tail: [...(goal.ladder ? [[{ text: goal.ladder.glyphs, role: "text" }] as Line] : []), [{ text: spent, role: "muted" }] as Line] }) };
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
/** Without a goal: whether a check has passed since this request's last edit. */
|
|
331
|
-
function checksLine(checks: NonNullable<StatusModel["checks"]>, w: number): Line {
|
|
332
|
-
if (checks.status === "unchecked" || !checks.command) return [{ text: "Checks", role: "text", bold: true }, { text: " · not run since last edit", role: "warning" }];
|
|
333
|
-
// partial: some of a declared contract holds; the rest may only be waiting for the user, so it is not a failure mark.
|
|
334
|
-
const mark: Seg[] = checks.status === "pass" ? [{ text: " ✓", role: "success" }] : checks.status === "partial" ? []
|
|
335
|
-
: checks.status === "fail" ? [{ text: " ✗", role: "error" }, ...(checks.exit === undefined ? [] : [{ text: ` exit ${checks.exit}`, role: "muted" as const }])]
|
|
336
|
-
: [{ text: " stale", role: "warning" }];
|
|
337
|
-
const room = Math.max(1, w - segsWidth(mark) - "Checks · ".length);
|
|
338
|
-
return [{ text: "Checks", role: "text", bold: true }, { text: ` · ${clipText(checks.command, room)}`, role: "muted" }, ...mark];
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
/** Without a sidebar the goal rides on the input box's bottom edge: `goal 2/3 · needs you · 4m 12s`. */
|
|
342
|
-
export function goalLabel(model: StatusModel): Label | undefined {
|
|
343
|
-
const goal = model.goal;
|
|
344
|
-
if (!goal) return undefined;
|
|
345
|
-
const passed = goal.checks.filter(check => check.status === "pass" || check.status === "accepted").length;
|
|
346
|
-
const parts = [`goal ${goal.phase === "drafting" ? "" : `${passed}/${goal.checks.length} · `}${PHASE[goal.phase].label}`,
|
|
347
|
-
...(goal.phase === "drafting" ? [] : [formatElapsed(goal.elapsedMs)])];
|
|
348
|
-
return { segs: [{ text: parts.join(" · "), role: goal.phase === "needs_you" ? "warning" : "muted" }], priority: 2, min: 8 };
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
function fileLine(file: FileStatus, w: number): Line {
|
|
352
|
-
const name = file.path.split("/").pop() || file.path;
|
|
353
|
-
const counts: Seg[] = file.edited && (file.added || file.removed)
|
|
354
|
-
? [{ text: `+${file.added}`, role: "success" }, { text: " ", role: "muted" }, { text: `−${file.removed}`, role: "error" }] : [];
|
|
355
|
-
const glyph: Seg = { text: file.edited ? "✎ " : "· ", role: file.edited ? "text" : "muted" };
|
|
356
|
-
if (!counts.length) return [glyph, { text: name, role: file.edited ? "text" : "muted" }];
|
|
357
|
-
const room = Math.max(1, w - 2 - segsWidth(counts) - 1);
|
|
358
|
-
const shown = clipText(name, room);
|
|
359
|
-
return [glyph, { text: shown, role: "text" }, { text: " ".repeat(Math.max(1, w - 2 - displayWidth(shown) - segsWidth(counts))), role: "muted" }, ...counts];
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
/** The leading characters of `text` that fit in `width` cells. */
|
|
363
|
-
function cells(text: string, width: number): string {
|
|
364
|
-
let out = "", used = 0;
|
|
365
|
-
for (const ch of text) { const w = displayWidth(ch); if (used + w > width) break; out += ch; used += w; }
|
|
366
|
-
return out;
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
/** Word-aware wrap (CJK breaks per character); at most `lines` rows, the last one cut with "…". */
|
|
370
|
-
export function wrap(text: string, width: number, lines: number): string[] {
|
|
371
|
-
const w = Math.max(1, width);
|
|
372
|
-
const tokens = text.match(/\s+|[⺀-鿿豈--]|[^\s⺀-鿿豈--]+/g) ?? [];
|
|
373
|
-
const out: string[] = [];
|
|
374
|
-
let current = "";
|
|
375
|
-
for (const token of tokens) {
|
|
376
|
-
if (/^\s+$/.test(token)) { if (current && displayWidth(current) < w) current += " "; continue; }
|
|
377
|
-
if (displayWidth(current + token) <= w) { current += token; continue; }
|
|
378
|
-
if (current.trim()) out.push(current.trimEnd());
|
|
379
|
-
current = displayWidth(token) > w ? clipText(token, w) : token;
|
|
380
|
-
}
|
|
381
|
-
if (current.trim()) out.push(current.trimEnd());
|
|
382
|
-
if (out.length <= lines) return out;
|
|
383
|
-
const kept = out.slice(0, lines);
|
|
384
|
-
const last = kept[lines - 1]!;
|
|
385
|
-
kept[lines - 1] = `${displayWidth(last) + 1 <= w ? last : cells(last, w - 1).trimEnd()}…`;
|
|
386
|
-
return kept;
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
/**
|
|
390
|
-
* The input box's top-edge label while no turn runs: the outcome in its own
|
|
391
|
-
* colour, then how long the last turn took (Codex "Worked for 2m 5s", Claude
|
|
392
|
-
* Code "Cooked for 1m 6s"). A plain grey "Turn finished" read the same as
|
|
393
|
-
* "nothing happened" (fix-finished-turn-status).
|
|
394
|
-
*/
|
|
395
|
-
export function idleActivityLabel(activity: NonNullable<StatusModel["activity"]>, turnMs?: number): Label {
|
|
396
|
-
const tone = activity.state === "done" ? { glyph: "✓", role: "success" as const }
|
|
397
|
-
: activity.state === "error" ? { glyph: "✗", role: "error" as const }
|
|
398
|
-
: activity.state === "blocked" ? { glyph: activity.label === "Stopped" ? "■" : "", role: "warning" as const }
|
|
399
|
-
: { glyph: "", role: "muted" as const };
|
|
400
|
-
const text = activity.label === "Stopped" && activity.detail ? `Stopped · ${activity.detail}` : activity.label;
|
|
401
|
-
return { segs: [...(tone.glyph ? [{ text: tone.glyph, role: tone.role }, { text: " ", role: "muted" as const }] : []), { text, role: tone.role },
|
|
402
|
-
...(turnMs !== undefined ? [{ text: ` · ${formatElapsed(turnMs)}`, role: "muted" as const }] : [])], priority: 1, min: 4, shrink: tone.glyph ? 2 : 0 };
|
|
403
|
-
}
|
package/src/tui/ui/styled.ts
DELETED
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Small helpers that build OpenTUI StyledText from parts, plus the bridge
|
|
3
|
-
* from ANSI lines (extension renderers, coloured shell output) to chunks.
|
|
4
|
-
*/
|
|
5
|
-
import { RGBA, StyledText, TextAttributes, type TextChunk } from "@opentui/core";
|
|
6
|
-
import { ansiSpans, type SpanColor } from "../ansi-spans.ts";
|
|
7
|
-
import { rgba } from "./colors.ts";
|
|
8
|
-
|
|
9
|
-
export interface Part {
|
|
10
|
-
readonly text: string;
|
|
11
|
-
/** An OpenTUI colour, or #rrggbb from a custom theme. */
|
|
12
|
-
readonly fg?: RGBA | string;
|
|
13
|
-
readonly bg?: RGBA | string;
|
|
14
|
-
readonly bold?: boolean;
|
|
15
|
-
readonly dim?: boolean;
|
|
16
|
-
readonly italic?: boolean;
|
|
17
|
-
readonly underline?: boolean;
|
|
18
|
-
/** Swap foreground and background (SGR 7): emphasis without painting a colour of our own. */
|
|
19
|
-
readonly inverse?: boolean;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* OpenTUI draws uncoloured text in opaque white. Text with no colour of its
|
|
24
|
-
* own is the terminal's foreground instead (SGR 39), which is what keeps it
|
|
25
|
-
* legible on a light terminal (refactor-tui-terminal-colors).
|
|
26
|
-
*/
|
|
27
|
-
const TERMINAL_TEXT: RGBA = RGBA.defaultForeground();
|
|
28
|
-
|
|
29
|
-
function chunk(text: string, fg?: RGBA, bg?: RGBA, attributes = 0, link?: string): TextChunk {
|
|
30
|
-
return { __isChunk: true, text, ...(fg ? { fg } : {}), ...(bg ? { bg } : {}), ...(attributes ? { attributes } : {}), ...(link ? { link: { url: link } } : {}) };
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export function parts(...items: (Part | string | false | undefined)[]): StyledText {
|
|
34
|
-
const chunks: TextChunk[] = [];
|
|
35
|
-
for (const item of items) {
|
|
36
|
-
if (!item) continue;
|
|
37
|
-
if (typeof item === "string") { chunks.push(chunk(item, TERMINAL_TEXT)); continue; }
|
|
38
|
-
let attributes = 0;
|
|
39
|
-
if (item.bold) attributes |= TextAttributes.BOLD;
|
|
40
|
-
if (item.dim) attributes |= TextAttributes.DIM;
|
|
41
|
-
if (item.italic) attributes |= TextAttributes.ITALIC;
|
|
42
|
-
if (item.underline) attributes |= TextAttributes.UNDERLINE;
|
|
43
|
-
if (item.inverse) attributes |= TextAttributes.INVERSE;
|
|
44
|
-
chunks.push(chunk(item.text, color(item.fg) ?? TERMINAL_TEXT, color(item.bg), attributes));
|
|
45
|
-
}
|
|
46
|
-
return new StyledText(chunks);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function color(value: RGBA | string | undefined): RGBA | undefined {
|
|
50
|
-
return value === undefined ? undefined : typeof value === "string" ? rgba(value) : value;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Indexed colours from shell output, diffs and highlighters stay indexed, so
|
|
55
|
-
* the terminal draws them from its own palette (refactor-tui-terminal-colors);
|
|
56
|
-
* converting them to xterm's default RGB would repaint them in someone else's.
|
|
57
|
-
*/
|
|
58
|
-
const indexedCache = new Map<number, RGBA>();
|
|
59
|
-
function spanColor(value: SpanColor | undefined): RGBA | undefined {
|
|
60
|
-
if (!value) return undefined;
|
|
61
|
-
if (value.kind === "rgb") return RGBA.fromInts(value.r, value.g, value.b, 255);
|
|
62
|
-
let ink = indexedCache.get(value.index);
|
|
63
|
-
if (!ink) { ink = RGBA.fromIndex(value.index); indexedCache.set(value.index, ink); }
|
|
64
|
-
return ink;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/** ANSI lines → one StyledText with newlines between rows. */
|
|
68
|
-
export function ansiText(lines: readonly string[]): StyledText {
|
|
69
|
-
const chunks: TextChunk[] = [];
|
|
70
|
-
lines.forEach((line, row) => {
|
|
71
|
-
if (row > 0) chunks.push(chunk("\n"));
|
|
72
|
-
for (const span of ansiSpans(line)) {
|
|
73
|
-
const s = span.style;
|
|
74
|
-
let attributes = 0;
|
|
75
|
-
if (s.bold) attributes |= TextAttributes.BOLD;
|
|
76
|
-
if (s.dim) attributes |= TextAttributes.DIM;
|
|
77
|
-
if (s.italic) attributes |= TextAttributes.ITALIC;
|
|
78
|
-
if (s.underline) attributes |= TextAttributes.UNDERLINE;
|
|
79
|
-
if (s.reverse) attributes |= TextAttributes.INVERSE;
|
|
80
|
-
if (s.strikethrough) attributes |= TextAttributes.STRIKETHROUGH;
|
|
81
|
-
chunks.push(chunk(span.text, spanColor(s.fg) ?? TERMINAL_TEXT, spanColor(s.bg), attributes, s.link));
|
|
82
|
-
}
|
|
83
|
-
});
|
|
84
|
-
return new StyledText(chunks);
|
|
85
|
-
}
|
package/src/tui/ui/tool-card.ts
DELETED
|
@@ -1,150 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tool call → card summary, evidence and expanded detail. Pure.
|
|
3
|
-
*
|
|
4
|
-
* The card's one-line summary comes from the call's input (what was asked);
|
|
5
|
-
* its evidence comes from the logged result (what happened): an exit code,
|
|
6
|
-
* a diff's +/- counts, a line count. A model's narration is never evidence
|
|
7
|
-
* (D11), so nothing here reads assistant text.
|
|
8
|
-
*/
|
|
9
|
-
import { diffLines, renderDiff, wrapPlain } from "../diff.ts";
|
|
10
|
-
import { ANSI, IDENTITY, PLAIN_ATTRS, paint, type Style } from "../style.ts";
|
|
11
|
-
import { displayWidth } from "../width.ts";
|
|
12
|
-
import type { ToolStatus } from "./items.ts";
|
|
13
|
-
|
|
14
|
-
export interface ToolCall { readonly name: string; readonly input: unknown; readonly result: { readonly text: string; readonly ok: boolean; readonly data?: unknown } | null }
|
|
15
|
-
export interface ToolCard { readonly summary: string; readonly status: ToolStatus; readonly evidence?: string }
|
|
16
|
-
|
|
17
|
-
function record(value: unknown): Record<string, unknown> { return value !== null && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : {}; }
|
|
18
|
-
function str(value: unknown): string { return typeof value === "string" ? value : value === undefined || value === null ? "" : JSON.stringify(value); }
|
|
19
|
-
|
|
20
|
-
export function exitCode(text: string): number | null {
|
|
21
|
-
const match = /^exit (-?\d+)/.exec(text);
|
|
22
|
-
return match ? Number(match[1]) : null;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function edits(input: Record<string, unknown>): { old: string; new: string }[] {
|
|
26
|
-
const list = Array.isArray(input["edits"]) ? input["edits"].map(record) : [input];
|
|
27
|
-
return list.map(edit => ({ old: str(edit["old_string"] ?? edit["old"]), new: str(edit["new_string"] ?? edit["new"]) }));
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export function diffCounts(input: unknown): { added: number; removed: number } {
|
|
31
|
-
let added = 0, removed = 0;
|
|
32
|
-
for (const edit of edits(record(input))) for (const op of diffLines(edit.old, edit.new)) { if (op.kind === "add") added++; else if (op.kind === "del") removed++; }
|
|
33
|
-
return { added, removed };
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/** One line: the argument a reader needs to recognise the call. */
|
|
37
|
-
export function toolSummary(name: string, input: unknown): string {
|
|
38
|
-
const args = record(input);
|
|
39
|
-
switch (name) {
|
|
40
|
-
case "bash": return str(args["command"]).split("\n")[0] ?? "";
|
|
41
|
-
case "read": {
|
|
42
|
-
const offset = typeof args["offset"] === "number" ? args["offset"] : undefined;
|
|
43
|
-
const limit = typeof args["limit"] === "number" ? args["limit"] : undefined;
|
|
44
|
-
return `${str(args["path"])}${offset !== undefined ? `:${offset}${limit !== undefined ? `+${limit}` : ""}` : ""}`;
|
|
45
|
-
}
|
|
46
|
-
case "write": case "edit": return str(args["path"]);
|
|
47
|
-
case "grep": case "find": case "glob": return [str(args["pattern"]), str(args["path"])].filter(Boolean).join(" in ");
|
|
48
|
-
case "history": return [str(args["section"]) || (args["recent"] ? "recent activity" : "session history"), typeof args["node"] === "number" ? `node ${args["node"]}` : ""].filter(Boolean).join(" · ");
|
|
49
|
-
default: {
|
|
50
|
-
const first = Object.entries(args).find(([, value]) => typeof value === "string" && value.length > 0);
|
|
51
|
-
return first ? `${first[0]}=${str(first[1]).split("\n")[0]}` : "";
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export function toolCard(call: ToolCall): ToolCard {
|
|
57
|
-
const summary = toolSummary(call.name, call.input);
|
|
58
|
-
if (!call.result) return { summary, status: "pending", ...(call.name === "edit" ? { evidence: plusMinus(call.input) } : {}) };
|
|
59
|
-
const { text, ok } = call.result;
|
|
60
|
-
const status: ToolStatus = ok ? "ok" : "error";
|
|
61
|
-
switch (call.name) {
|
|
62
|
-
case "bash": {
|
|
63
|
-
const code = exitCode(text);
|
|
64
|
-
return { summary, status, ...(code !== null ? { evidence: `exit ${code}` } : ok ? {} : { evidence: "failed" }) };
|
|
65
|
-
}
|
|
66
|
-
case "edit": return { summary, status, evidence: ok ? plusMinus(call.input) : "failed" };
|
|
67
|
-
case "write": {
|
|
68
|
-
const lines = str(record(call.input)["content"]).split("\n").length;
|
|
69
|
-
return { summary, status, evidence: ok ? `${lines} lines` : "failed" };
|
|
70
|
-
}
|
|
71
|
-
case "read": {
|
|
72
|
-
const data = record(call.result.data), start = data["start"], end = data["end"];
|
|
73
|
-
const lines = typeof start === "number" && typeof end === "number" && end >= start ? end - start + 1 : text ? text.split("\n").length : 0;
|
|
74
|
-
return { summary, status, evidence: ok ? `${lines} lines` : "failed" };
|
|
75
|
-
}
|
|
76
|
-
case "history": {
|
|
77
|
-
const data = record(jsonValue(text));
|
|
78
|
-
return { summary, status, ...(ok && typeof data["matches"] === "number" ? { evidence: `${data["matches"]} matches` } : ok ? {} : { evidence: "failed" }) };
|
|
79
|
-
}
|
|
80
|
-
default: return { summary, status, ...(ok ? {} : { evidence: "failed" }) };
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
function plusMinus(input: unknown): string {
|
|
85
|
-
const { added, removed } = diffCounts(input);
|
|
86
|
-
return `+${added} −${removed}`;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/** Expanded body: input that matters, then the result. Width-bounded ANSI lines. */
|
|
90
|
-
export function toolDetail(call: ToolCall, width: number, style: Style = ANSI): string[] {
|
|
91
|
-
const w = Math.max(1, width);
|
|
92
|
-
// Semantic colour, never SGR faint: faint made already-muted arguments unreadable.
|
|
93
|
-
const dim = (text: string) => style.token?.("muted", text) ?? text;
|
|
94
|
-
const red = (text: string) => style === IDENTITY ? text : paint(text, { ...PLAIN_ATTRS, error: true }, style);
|
|
95
|
-
const clip = (line: string) => truncate(line, w);
|
|
96
|
-
const args = record(call.input);
|
|
97
|
-
const out: string[] = [];
|
|
98
|
-
switch (call.name) {
|
|
99
|
-
case "bash": out.push(...str(args["command"]).split("\n").map(line => clip(`$ ${line}`))); break;
|
|
100
|
-
case "edit": for (const edit of edits(args)) out.push(...renderDiff(edit.old, edit.new, w, style)); break;
|
|
101
|
-
case "write": out.push(...str(args["content"]).split("\n").flatMap(line => wrapPlain(line, w)).map(dim)); break;
|
|
102
|
-
case "read": break;
|
|
103
|
-
default: {
|
|
104
|
-
const json = JSON.stringify(call.input, null, 2) ?? "";
|
|
105
|
-
out.push(...json.split("\n").flatMap(line => wrapPlain(line, w)).map(dim));
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
if (call.result) {
|
|
109
|
-
const body = call.name === "bash" ? call.result.text.replace(/^exit -?\d+[^\n]*\n?/, "") : call.result.text;
|
|
110
|
-
const parsed = jsonValue(body);
|
|
111
|
-
const readable = parsed !== undefined ? JSON.stringify(parsed, null, 2) : body;
|
|
112
|
-
const lines = readable ? readable.replace(/\s+$/, "").split("\n").flatMap(line => wrapPlain(line, w)) : [];
|
|
113
|
-
if (lines.length && out.length) out.push("");
|
|
114
|
-
const paintLine = call.result.ok ? (line: string) => clip(line) : (line: string) => red(clip(line));
|
|
115
|
-
out.push(...lines.map(paintLine));
|
|
116
|
-
} else out.push(dim("running…"));
|
|
117
|
-
return out;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
function jsonValue(text: string): unknown {
|
|
121
|
-
if (!/^[\s]*[\[{]/.test(text)) return undefined;
|
|
122
|
-
try { return JSON.parse(text) as unknown; } catch { return undefined; }
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
/** Brief logged output stays visible even when details are folded. */
|
|
126
|
-
export function toolPreview(call: ToolCall): string[] {
|
|
127
|
-
if (!call.result) return [];
|
|
128
|
-
let raw = call.result.text.replace(/^exit -?\d+[^\n]*\n?/, "").trim();
|
|
129
|
-
// The read result's provenance remains in expanded details. Its preview
|
|
130
|
-
// shows the selected source lines instead of an opaque version fingerprint.
|
|
131
|
-
if (call.name === "read") raw = raw.replace(/^\[Read selection:[^\n]*\nsha256:[^\n]*\n/, "");
|
|
132
|
-
const parsed = jsonValue(raw), data = record(parsed);
|
|
133
|
-
if (Array.isArray(data["items"])) {
|
|
134
|
-
const items = data["items"] as unknown[];
|
|
135
|
-
return items.slice(0, 2).map(value => { const item = record(value); return str(item["text"] ?? item["what"] ?? item["path"] ?? item["kind"] ?? value).replace(/\s+/g, " "); });
|
|
136
|
-
}
|
|
137
|
-
if (parsed !== undefined) return Object.entries(data).filter(([key]) => key !== "source").slice(0, 2).map(([key, value]) => `${key} ${typeof value === "object" && value !== null ? Array.isArray(value) ? `${value.length} items` : Object.keys(value).join(", ") : str(value)}`);
|
|
138
|
-
return raw.split("\n").filter(Boolean).slice(0, 2);
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
function truncate(line: string, width: number): string {
|
|
142
|
-
if (displayWidth(line) <= width) return line;
|
|
143
|
-
let out = "", used = 0;
|
|
144
|
-
for (const ch of line) {
|
|
145
|
-
const w = displayWidth(ch);
|
|
146
|
-
if (used + w > width - 1) break;
|
|
147
|
-
out += ch; used += w;
|
|
148
|
-
}
|
|
149
|
-
return out + "…";
|
|
150
|
-
}
|