@visual-z/xeno 0.2.1 → 0.3.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/CHANGELOG.md +9 -0
- package/README.md +106 -26
- package/docs/install.md +51 -13
- package/docs/providers.md +11 -0
- package/package.json +5 -4
- package/src/apps/approve.ts +3 -3
- package/src/apps/commands.ts +38 -5
- package/src/apps/completions.ts +73 -19
- package/src/apps/image-draft.ts +46 -0
- package/src/apps/project-trust.ts +2 -42
- package/src/apps/provider-connect.ts +346 -0
- package/src/apps/render.ts +22 -47
- package/src/apps/resources.ts +5 -2
- package/src/apps/shell.ts +298 -1526
- package/src/apps/startup-input.ts +45 -9
- package/src/apps/startup.ts +128 -86
- package/src/apps/ui/app.ts +1383 -0
- package/src/apps/ui/extension-host.ts +194 -0
- package/src/apps/ui/image-cache.ts +78 -0
- package/src/apps/ui/palette.ts +88 -0
- package/src/apps/ui/project.ts +127 -0
- package/src/apps/ui/standalone.ts +146 -0
- package/src/apps/ui/status-model.ts +68 -0
- package/src/apps/ui/surface.ts +84 -0
- package/src/apps/ui/theme.ts +86 -0
- package/src/core/acceptance.ts +107 -0
- package/src/core/action-defaults.ts +47 -295
- package/src/core/benchmark.ts +2 -2
- package/src/core/branch-receipt.ts +24 -0
- package/src/core/checkpoints.ts +64 -0
- package/src/core/churn.ts +2 -0
- package/src/core/command-artifacts.ts +94 -0
- package/src/core/command-contract.ts +35 -0
- package/src/core/completion-review.ts +21 -5
- package/src/core/context-detail.ts +37 -0
- package/src/core/context-policy.ts +74 -1
- package/src/core/continuation.ts +17 -2
- package/src/core/derive.ts +29 -3
- package/src/core/draft-images.ts +56 -0
- package/src/core/environment.ts +145 -0
- package/src/core/evaluation-coverage.ts +74 -0
- package/src/core/event-validation.ts +80 -5
- package/src/core/events.ts +97 -3
- package/src/core/evidence-links.ts +7 -0
- package/src/core/evidence.ts +13 -3
- package/src/core/execution.ts +8 -2
- package/src/core/experiment.ts +133 -0
- package/src/core/export.ts +9 -0
- package/src/core/failure-recovery.ts +173 -0
- package/src/core/file-precondition.ts +72 -0
- package/src/core/handoff.ts +65 -4
- package/src/core/history-query.ts +38 -2
- package/src/core/history.ts +38 -1
- package/src/core/integration-health.ts +34 -0
- package/src/core/keymap.ts +8 -20
- package/src/core/model-picker.ts +64 -0
- package/src/core/native-eval.ts +25 -0
- package/src/core/note-guidance.ts +34 -0
- package/src/core/observation-view.ts +49 -8
- package/src/core/permissions.ts +24 -2
- package/src/core/process-receipt.ts +105 -0
- package/src/core/process-state.ts +37 -0
- package/src/core/project.ts +45 -18
- package/src/core/prompt-policy.ts +37 -0
- package/src/core/query-eval-contract.ts +14 -0
- package/src/core/read-observation.ts +82 -0
- package/src/core/recovery-corpus.ts +17 -0
- package/src/core/recovery-evaluation.ts +60 -0
- package/src/core/recovery-sources.ts +74 -0
- package/src/core/runtime-target.ts +94 -0
- package/src/core/search-observation.ts +97 -0
- package/src/core/session-tree.ts +147 -0
- package/src/core/settings.ts +26 -10
- package/src/core/skills.ts +44 -0
- package/src/core/sql-query.ts +211 -0
- package/src/core/theme.ts +36 -58
- package/src/core/tool-contract.ts +51 -2
- package/src/core/tool-discovery.ts +87 -0
- package/src/core/tool-failure.ts +60 -0
- package/src/core/tool-mode-protocol.ts +25 -0
- package/src/core/transcript.ts +19 -4
- package/src/core/trial-lifecycle.ts +14 -0
- package/src/core/tui-mode.ts +29 -0
- package/src/core/update-check.ts +97 -0
- package/src/core/version.ts +55 -0
- package/src/core/work-state.ts +49 -3
- package/src/core/working-set.ts +91 -0
- package/src/runtime/auth.ts +2 -2
- package/src/runtime/branch-jobs.ts +36 -0
- package/src/runtime/clipboard.ts +15 -4
- package/src/runtime/command-artifacts.ts +64 -0
- package/src/runtime/environment-tools.ts +60 -0
- package/src/runtime/evidence-notes.ts +4 -2
- package/src/runtime/exec-sessions.ts +206 -61
- package/src/runtime/execution-control.ts +1 -0
- package/src/runtime/extension-ui.ts +22 -30
- package/src/runtime/extensions.ts +95 -16
- package/src/runtime/harness-comparison.ts +27 -7
- package/src/runtime/harness-eval.ts +460 -0
- package/src/runtime/history-cache.ts +57 -0
- package/src/runtime/history-tools.ts +62 -12
- package/src/runtime/integrations.ts +210 -0
- package/src/runtime/loop.ts +1021 -268
- package/src/runtime/model-discovery.ts +208 -0
- package/src/runtime/native-eval-fixtures.ts +33 -0
- package/src/runtime/native-eval.ts +143 -0
- package/src/runtime/oauth.ts +1 -1
- package/src/runtime/provider-connect.ts +109 -0
- package/src/runtime/provider-presets.ts +87 -0
- package/src/runtime/query-eval-verifier.ts +28 -0
- package/src/runtime/recovery-coverage.ts +71 -0
- package/src/runtime/recovery-evidence.ts +76 -0
- package/src/runtime/recovery-fixtures.ts +116 -0
- package/src/runtime/runtime-target.ts +149 -0
- package/src/runtime/tool-broker.ts +137 -0
- package/src/runtime/tool-discovery.ts +57 -0
- package/src/runtime/tool-mode-eval.ts +146 -0
- package/src/runtime/tool-queue.ts +77 -0
- package/src/runtime/tools.ts +628 -183
- package/src/runtime/trial-control.ts +45 -0
- package/src/runtime/update-check.ts +47 -0
- package/src/runtime/window-trial.ts +2 -1
- package/src/runtime/working-notes.ts +33 -3
- package/src/runtime/workspace-lifecycle.ts +97 -0
- package/src/store/acceptance.ts +15 -0
- package/src/store/checkpoint-history.ts +43 -0
- package/src/store/command-artifacts.ts +96 -0
- package/src/store/command.ts +65 -37
- package/src/store/file-checkpoints.ts +104 -0
- package/src/store/file-index.ts +13 -1
- package/src/store/git-checkpoints.ts +67 -0
- package/src/store/observations.ts +61 -20
- package/src/store/process-receipts.ts +34 -0
- package/src/store/query-source.ts +13 -8
- package/src/store/recovery.ts +2 -3
- package/src/store/replay.ts +16 -4
- package/src/store/runtime-target.ts +64 -0
- package/src/store/search.ts +171 -132
- package/src/store/session-archive.ts +4 -0
- package/src/store/source-cancellation.ts +6 -0
- package/src/store/update-cache.ts +36 -0
- package/src/store/workspace-observation.ts +113 -0
- package/src/tui/ansi-spans.ts +126 -0
- package/src/tui/style.ts +19 -16
- package/src/tui/ui/atomic-draft.ts +121 -0
- package/src/tui/ui/canvas.ts +96 -0
- package/src/tui/ui/colors.ts +70 -0
- package/src/tui/ui/composer.ts +199 -0
- package/src/tui/ui/dialog.ts +379 -0
- package/src/tui/ui/draft.ts +23 -0
- package/src/tui/ui/edge.ts +186 -0
- package/src/tui/ui/image-size.ts +40 -0
- package/src/tui/ui/ink.ts +210 -0
- package/src/tui/ui/items.ts +40 -0
- package/src/tui/ui/keys.ts +40 -0
- package/src/tui/ui/layout.ts +74 -0
- package/src/tui/ui/list-model.ts +86 -0
- package/src/tui/ui/list-view.ts +172 -0
- package/src/tui/ui/mascot.ts +67 -0
- package/src/tui/ui/messages.ts +518 -0
- package/src/tui/ui/motion.ts +28 -0
- package/src/tui/ui/paths.ts +10 -0
- package/src/tui/ui/pictures.ts +78 -0
- package/src/tui/ui/screen.ts +421 -0
- package/src/tui/ui/status.ts +271 -0
- package/src/tui/ui/styled.ts +85 -0
- package/src/tui/ui/testing.ts +5 -0
- package/src/tui/ui/tool-card.ts +150 -0
- package/src/apps/extension-ui.ts +0 -330
- package/src/apps/resource-browser.ts +0 -75
- package/src/apps/session-browser.ts +0 -113
- package/src/apps/session-picker.ts +0 -55
- package/src/tui/ansi.ts +0 -67
- package/src/tui/autocomplete.ts +0 -29
- package/src/tui/chat-viewport.ts +0 -47
- package/src/tui/complete.ts +0 -87
- package/src/tui/editor.ts +0 -940
- package/src/tui/host.ts +0 -264
- package/src/tui/input.ts +0 -75
- package/src/tui/keys.ts +0 -378
- package/src/tui/markdown.ts +0 -17
- package/src/tui/render.ts +0 -153
- package/src/tui/select.ts +0 -258
- package/src/tui/settings.ts +0 -44
- package/src/tui/statusline.ts +0 -162
- package/src/tui/system-theme.ts +0 -700
- package/src/tui/terminal.ts +0 -30
- package/src/tui/text.ts +0 -52
- package/src/tui/toolview.ts +0 -386
- package/src/tui/transcript.ts +0 -123
- package/src/tui/tree.ts +0 -471
- package/src/vendor/pi-tui/LICENSE.upstream +0 -21
- package/src/vendor/pi-tui/UPSTREAM.json +0 -50
- package/src/vendor/pi-tui/VENDOR.md +0 -25
- package/src/vendor/pi-tui/alt-screen-search.ts +0 -327
- package/src/vendor/pi-tui/autocomplete.ts +0 -860
- package/src/vendor/pi-tui/colors.ts +0 -367
- package/src/vendor/pi-tui/components/alt-screen-flash.ts +0 -51
- package/src/vendor/pi-tui/components/box.ts +0 -166
- package/src/vendor/pi-tui/components/cancellable-loader.ts +0 -40
- package/src/vendor/pi-tui/components/editor.ts +0 -2472
- package/src/vendor/pi-tui/components/h-stack.ts +0 -44
- package/src/vendor/pi-tui/components/image.ts +0 -127
- package/src/vendor/pi-tui/components/input.ts +0 -494
- package/src/vendor/pi-tui/components/loader.ts +0 -101
- package/src/vendor/pi-tui/components/markdown.ts +0 -1021
- package/src/vendor/pi-tui/components/mouse-region.ts +0 -33
- package/src/vendor/pi-tui/components/scroll-view.ts +0 -224
- package/src/vendor/pi-tui/components/select-list.ts +0 -273
- package/src/vendor/pi-tui/components/settings-list.ts +0 -328
- package/src/vendor/pi-tui/components/spacer.ts +0 -28
- package/src/vendor/pi-tui/components/stack.ts +0 -154
- package/src/vendor/pi-tui/components/text.ts +0 -107
- package/src/vendor/pi-tui/components/truncated-text.ts +0 -65
- package/src/vendor/pi-tui/components/v-stack.ts +0 -33
- package/src/vendor/pi-tui/editor-component.ts +0 -74
- package/src/vendor/pi-tui/fuzzy.ts +0 -138
- package/src/vendor/pi-tui/index.ts +0 -186
- package/src/vendor/pi-tui/keybindings.ts +0 -320
- package/src/vendor/pi-tui/keys.ts +0 -1401
- package/src/vendor/pi-tui/kill-ring.ts +0 -46
- package/src/vendor/pi-tui/latex.ts +0 -1506
- package/src/vendor/pi-tui/layout-node.ts +0 -51
- package/src/vendor/pi-tui/layout.ts +0 -449
- package/src/vendor/pi-tui/native/UPSTREAM.json +0 -23
- package/src/vendor/pi-tui/native/clipboard.h +0 -208
- package/src/vendor/pi-tui/native/darwin/README.md +0 -22
- package/src/vendor/pi-tui/native/darwin/build.sh +0 -69
- package/src/vendor/pi-tui/native/darwin/prebuilds/darwin-arm64/darwin-platform.node +0 -0
- package/src/vendor/pi-tui/native/darwin/prebuilds/darwin-x64/darwin-platform.node +0 -0
- package/src/vendor/pi-tui/native/darwin/src/darwin-platform.m +0 -132
- package/src/vendor/pi-tui/native/linux/README.md +0 -23
- package/src/vendor/pi-tui/native/linux/build.sh +0 -65
- package/src/vendor/pi-tui/native/linux/prebuilds/linux-arm64/linux-platform-x11.node +0 -0
- package/src/vendor/pi-tui/native/linux/prebuilds/linux-x64/linux-platform-x11.node +0 -0
- package/src/vendor/pi-tui/native/linux/src/clipboard-worker.h +0 -70
- package/src/vendor/pi-tui/native/linux/src/linux-platform-x11.c +0 -274
- package/src/vendor/pi-tui/native/napi.h +0 -91
- package/src/vendor/pi-tui/native/win32/README.md +0 -31
- package/src/vendor/pi-tui/native/win32/build.mjs +0 -232
- package/src/vendor/pi-tui/native/win32/prebuilds/win32-arm64/win32-platform.node +0 -0
- package/src/vendor/pi-tui/native/win32/prebuilds/win32-x64/win32-platform.node +0 -0
- package/src/vendor/pi-tui/native/win32/src/win32-platform.c +0 -241
- package/src/vendor/pi-tui/native-modifiers.ts +0 -13
- package/src/vendor/pi-tui/native-module-path.ts +0 -31
- package/src/vendor/pi-tui/native-platform.ts +0 -65
- package/src/vendor/pi-tui/oklab.ts +0 -233
- package/src/vendor/pi-tui/stdin-buffer.ts +0 -444
- package/src/vendor/pi-tui/terminal-colors.ts +0 -91
- package/src/vendor/pi-tui/terminal-image.ts +0 -731
- package/src/vendor/pi-tui/terminal.ts +0 -554
- package/src/vendor/pi-tui/tui-alt-screen.ts +0 -1750
- package/src/vendor/pi-tui/tui-main-screen.ts +0 -655
- package/src/vendor/pi-tui/tui.ts +0 -1493
- package/src/vendor/pi-tui/undo-stack.ts +0 -28
- package/src/vendor/pi-tui/utils.ts +0 -1385
- package/src/vendor/pi-tui/wheel-scroll.ts +0 -82
- package/src/vendor/pi-tui/word-navigation.ts +0 -117
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a list dialog shows for a query. Pure.
|
|
3
|
+
*
|
|
4
|
+
* Every picker in the UI (palette, model, login, resume, tree, settings, …)
|
|
5
|
+
* is the same shape: items grouped into sections, a fuzzy query, one
|
|
6
|
+
* selected row, optional checkboxes. One model means one set of keys and
|
|
7
|
+
* one place where "Enter confirms the row under the cursor" is proven.
|
|
8
|
+
*
|
|
9
|
+
* With an empty query, items keep their given order and each new section
|
|
10
|
+
* gets a header row (OpenCode's dialog-model). With a query, sections
|
|
11
|
+
* flatten into one ranked list: grouping a three-hit search only adds noise.
|
|
12
|
+
*/
|
|
13
|
+
import { fuzzyMatch } from "../fuzzy.ts";
|
|
14
|
+
|
|
15
|
+
export interface ListItem {
|
|
16
|
+
readonly value: string;
|
|
17
|
+
readonly label: string;
|
|
18
|
+
readonly description?: string;
|
|
19
|
+
readonly section?: string;
|
|
20
|
+
/** Right-aligned hint: a key chord, "current", a count. */
|
|
21
|
+
readonly hint?: string;
|
|
22
|
+
/** Extra text matched by the query but not shown. */
|
|
23
|
+
readonly keywords?: string;
|
|
24
|
+
readonly disabled?: boolean;
|
|
25
|
+
/** Indentation (tree rows). */
|
|
26
|
+
readonly depth?: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export type ListRow =
|
|
30
|
+
| { readonly kind: "header"; readonly text: string }
|
|
31
|
+
| { readonly kind: "item"; readonly item: ListItem; readonly index: number };
|
|
32
|
+
|
|
33
|
+
export function filterItems(items: readonly ListItem[], query: string): ListItem[] {
|
|
34
|
+
const q = query.trim();
|
|
35
|
+
if (!q) return [...items];
|
|
36
|
+
const ranked: { item: ListItem; score: number; index: number }[] = [];
|
|
37
|
+
items.forEach((item, index) => {
|
|
38
|
+
const label = fuzzyMatch(item.label, q);
|
|
39
|
+
const rest = fuzzyMatch(`${item.label} ${item.description ?? ""} ${item.section ?? ""} ${item.keywords ?? ""}`, q);
|
|
40
|
+
// A hit in the label outranks a hit that needed the description.
|
|
41
|
+
const score = label !== null ? label : rest !== null ? rest + 1000 : null;
|
|
42
|
+
if (score !== null) ranked.push({ item, score, index });
|
|
43
|
+
});
|
|
44
|
+
ranked.sort((a, b) => a.score - b.score || a.index - b.index);
|
|
45
|
+
return ranked.map(entry => entry.item);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function listRows(items: readonly ListItem[], query: string): ListRow[] {
|
|
49
|
+
const filtered = filterItems(items, query);
|
|
50
|
+
const rows: ListRow[] = [];
|
|
51
|
+
let section: string | undefined;
|
|
52
|
+
const grouped = !query.trim();
|
|
53
|
+
filtered.forEach((item, index) => {
|
|
54
|
+
if (grouped && item.section && item.section !== section) rows.push({ kind: "header", text: item.section });
|
|
55
|
+
section = item.section;
|
|
56
|
+
rows.push({ kind: "item", item, index });
|
|
57
|
+
});
|
|
58
|
+
return rows;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Next selectable index (wrapping), skipping disabled items. -1 when none. */
|
|
62
|
+
export function step(items: readonly ListItem[], from: number, delta: number): number {
|
|
63
|
+
const n = items.length;
|
|
64
|
+
if (n === 0) return -1;
|
|
65
|
+
for (let k = 1; k <= n; k++) {
|
|
66
|
+
const candidate = (((from + delta * k) % n) + n) % n;
|
|
67
|
+
if (!items[candidate]!.disabled) return candidate;
|
|
68
|
+
}
|
|
69
|
+
return -1;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** First enabled index at or after `from`, used when a query changes the list. */
|
|
73
|
+
export function firstEnabled(items: readonly ListItem[], from = 0): number {
|
|
74
|
+
for (let i = Math.max(0, from); i < items.length; i++) if (!items[i]!.disabled) return i;
|
|
75
|
+
for (let i = 0; i < Math.min(from, items.length); i++) if (!items[i]!.disabled) return i;
|
|
76
|
+
return -1;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Rows to draw so the selected item stays visible, keeping its header when it fits. */
|
|
80
|
+
export function windowRows(rows: readonly ListRow[], selected: number, height: number): { start: number; end: number } {
|
|
81
|
+
if (rows.length <= height) return { start: 0, end: rows.length };
|
|
82
|
+
const at = Math.max(0, rows.findIndex(row => row.kind === "item" && row.index === selected));
|
|
83
|
+
let start = Math.max(0, Math.min(at - Math.floor(height / 2), rows.length - height));
|
|
84
|
+
if (height > 1 && start > 0 && rows[at - 1]?.kind === "header" && at - 1 < start) start = at - 1;
|
|
85
|
+
return { start, end: Math.min(rows.length, start + height) };
|
|
86
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one list component: grouped rows, selection, optional checkboxes,
|
|
3
|
+
* right-aligned hints, and a search field on the bottom row — the row the
|
|
4
|
+
* draft occupies when no picker is open, so the input box keeps its shape
|
|
5
|
+
* (docs/tasks/refactor-tui-dock-layout.md). State lives in list-model.ts
|
|
6
|
+
* (pure); this class draws it and exposes the moves the dialog host routes.
|
|
7
|
+
*
|
|
8
|
+
* Rows are a Canvas: moving the selection repaints cells, not nodes. The
|
|
9
|
+
* search text is a focused Input so cursor, paste and word deletion stay
|
|
10
|
+
* OpenTUI's.
|
|
11
|
+
*/
|
|
12
|
+
import { BoxRenderable, InputRenderable, InputRenderableEvents, type RenderContext } from "@opentui/core";
|
|
13
|
+
import { Canvas } from "./canvas.ts";
|
|
14
|
+
import type { UiColors } from "./colors.ts";
|
|
15
|
+
import { clipText, segsWidth, splitLine, type Seg } from "./edge.ts";
|
|
16
|
+
import { filterItems, firstEnabled, listRows, step, windowRows, type ListItem, type ListRow } from "./list-model.ts";
|
|
17
|
+
import type { Line } from "./status.ts";
|
|
18
|
+
|
|
19
|
+
export interface ListViewOptions {
|
|
20
|
+
readonly colors: UiColors;
|
|
21
|
+
readonly rows?: number;
|
|
22
|
+
readonly search?: boolean;
|
|
23
|
+
readonly multi?: boolean;
|
|
24
|
+
readonly placeholder?: string;
|
|
25
|
+
readonly empty?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Visible rows. Ten keeps a picker under half of a 30-row terminal. Assumed. */
|
|
29
|
+
const DEFAULT_ROWS = 10;
|
|
30
|
+
let serial = 0;
|
|
31
|
+
|
|
32
|
+
export class ListView extends BoxRenderable {
|
|
33
|
+
private colors: UiColors;
|
|
34
|
+
private maxRows: number;
|
|
35
|
+
private readonly multi: boolean;
|
|
36
|
+
private readonly emptyText: string;
|
|
37
|
+
readonly searchInput: InputRenderable | undefined;
|
|
38
|
+
private readonly searchRow: BoxRenderable | undefined;
|
|
39
|
+
private readonly searchGlyph: Canvas | undefined;
|
|
40
|
+
private readonly body: Canvas;
|
|
41
|
+
private items: ListItem[] = [];
|
|
42
|
+
private filtered: ListItem[] = [];
|
|
43
|
+
private rowsModel: ListRow[] = [];
|
|
44
|
+
private selected = -1;
|
|
45
|
+
readonly checked = new Set<string>();
|
|
46
|
+
onChange: (() => void) | undefined;
|
|
47
|
+
|
|
48
|
+
constructor(ctx: RenderContext, options: ListViewOptions) {
|
|
49
|
+
const id = `list-${serial++}`;
|
|
50
|
+
super(ctx, { id, flexDirection: "column", flexShrink: 0 });
|
|
51
|
+
this.colors = options.colors;
|
|
52
|
+
this.maxRows = options.rows ?? DEFAULT_ROWS;
|
|
53
|
+
this.multi = options.multi ?? false;
|
|
54
|
+
this.emptyText = options.empty ?? "No matches";
|
|
55
|
+
this.body = new Canvas(ctx, { id: `${id}-rows`, height: 1, flexShrink: 0, colors: this.colors });
|
|
56
|
+
this.add(this.body);
|
|
57
|
+
if (options.search !== false) {
|
|
58
|
+
this.searchRow = new BoxRenderable(ctx, { id: `${id}-search`, flexDirection: "row", height: 1, marginTop: 1, flexShrink: 0 });
|
|
59
|
+
this.searchGlyph = new Canvas(ctx, { id: `${id}-glyph`, width: 2, height: 1, flexShrink: 0, colors: this.colors });
|
|
60
|
+
this.searchGlyph.setRows([[{ text: "›", role: "accent", bold: true }]]);
|
|
61
|
+
this.searchInput = new InputRenderable(ctx, { id: `${id}-input`, flexGrow: 1, placeholder: options.placeholder ?? "Search",
|
|
62
|
+
textColor: this.colors.text, focusedTextColor: this.colors.text, placeholderColor: this.colors.muted, backgroundColor: "transparent", focusedBackgroundColor: "transparent" });
|
|
63
|
+
this.searchInput.on(InputRenderableEvents.INPUT, () => this.refilter());
|
|
64
|
+
this.searchRow.add(this.searchGlyph); this.searchRow.add(this.searchInput);
|
|
65
|
+
this.add(this.searchRow);
|
|
66
|
+
}
|
|
67
|
+
this.onSizeChange = () => this.paint();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
get query(): string { return this.searchInput?.value ?? ""; }
|
|
71
|
+
get current(): ListItem | undefined { return this.selected >= 0 ? this.filtered[this.selected] : undefined; }
|
|
72
|
+
get visibleItems(): readonly ListItem[] { return this.filtered; }
|
|
73
|
+
/** "3/40" when the list scrolls, for the box's top edge; "" when everything is visible. */
|
|
74
|
+
get scrollPosition(): string { return this.rowsModel.length > this.maxRows && this.selected >= 0 ? `${this.selected + 1}/${this.filtered.length}` : ""; }
|
|
75
|
+
/** Plain text of the drawn rows (tests). */
|
|
76
|
+
get text(): string { return this.body.text; }
|
|
77
|
+
|
|
78
|
+
setColors(colors: UiColors): void {
|
|
79
|
+
this.colors = colors;
|
|
80
|
+
this.body.setColors(colors);
|
|
81
|
+
this.searchGlyph?.setColors(colors);
|
|
82
|
+
if (this.searchInput) {
|
|
83
|
+
this.searchInput.textColor = colors.text;
|
|
84
|
+
this.searchInput.focusedTextColor = colors.text;
|
|
85
|
+
this.searchInput.placeholderColor = colors.muted;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
setRowLimit(rows: number): void {
|
|
90
|
+
const limit = Math.max(1, rows);
|
|
91
|
+
if (limit === this.maxRows) return;
|
|
92
|
+
this.maxRows = limit; this.paint();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
setItems(items: readonly ListItem[], selectedValue?: string): void {
|
|
96
|
+
const keep = selectedValue ?? this.current?.value;
|
|
97
|
+
this.items = [...items];
|
|
98
|
+
this.refilter(keep);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
setQuery(query: string): void {
|
|
102
|
+
if (this.searchInput) this.searchInput.value = query;
|
|
103
|
+
this.refilter();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
move(delta: number): void {
|
|
107
|
+
const next = step(this.filtered, this.selected, delta);
|
|
108
|
+
if (next >= 0) this.selected = next;
|
|
109
|
+
this.paint();
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
page(delta: number): void {
|
|
113
|
+
const target = Math.max(0, Math.min(this.filtered.length - 1, this.selected + delta * this.maxRows));
|
|
114
|
+
this.selected = firstEnabled(this.filtered, target);
|
|
115
|
+
this.paint();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
toggle(): void {
|
|
119
|
+
const item = this.current;
|
|
120
|
+
if (!this.multi || !item || item.disabled) return;
|
|
121
|
+
if (this.checked.has(item.value)) this.checked.delete(item.value); else this.checked.add(item.value);
|
|
122
|
+
this.paint();
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
toggleAllVisible(): void {
|
|
126
|
+
if (!this.multi) return;
|
|
127
|
+
const enabled = this.filtered.filter(item => !item.disabled);
|
|
128
|
+
const all = enabled.every(item => this.checked.has(item.value));
|
|
129
|
+
for (const item of enabled) { if (all) this.checked.delete(item.value); else this.checked.add(item.value); }
|
|
130
|
+
this.paint();
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
private refilter(keep?: string): void {
|
|
134
|
+
this.filtered = filterItems(this.items, this.query);
|
|
135
|
+
this.rowsModel = listRows(this.items, this.query);
|
|
136
|
+
const at = keep === undefined ? -1 : this.filtered.findIndex(item => item.value === keep);
|
|
137
|
+
this.selected = at >= 0 && !this.filtered[at]!.disabled ? at : firstEnabled(this.filtered);
|
|
138
|
+
this.paint();
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
private paint(): void {
|
|
142
|
+
const width = Math.max(10, this.width || 60);
|
|
143
|
+
const lines: Line[] = [];
|
|
144
|
+
if (this.rowsModel.length === 0) lines.push([{ text: this.emptyText, role: "muted" }]);
|
|
145
|
+
else {
|
|
146
|
+
const { start, end } = windowRows(this.rowsModel, this.selected, this.maxRows);
|
|
147
|
+
for (let r = start; r < end; r++) lines.push(this.line(this.rowsModel[r]!, width));
|
|
148
|
+
}
|
|
149
|
+
this.body.height = lines.length;
|
|
150
|
+
this.body.setRows(lines);
|
|
151
|
+
this.onChange?.();
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
private line(row: ListRow, width: number): Line {
|
|
155
|
+
if (row.kind === "header") return [{ text: row.text.toUpperCase(), role: "muted", bold: true }];
|
|
156
|
+
const item = row.item, selected = row.index === this.selected;
|
|
157
|
+
const lead: Seg[] = [
|
|
158
|
+
{ text: selected ? "▸ " : " ", role: "accent", bold: true },
|
|
159
|
+
...(this.multi ? [{ text: this.checked.has(item.value) ? "◉ " : "○ ", role: this.checked.has(item.value) ? "success" as const : "muted" as const }] : []),
|
|
160
|
+
...(item.depth ? [{ text: " ".repeat(item.depth), role: "text" as const }] : []),
|
|
161
|
+
];
|
|
162
|
+
const label: Seg = { text: item.label, role: item.disabled ? "muted" : "text", bold: selected };
|
|
163
|
+
// Keep the actionable identity before optional detail on narrow boxes.
|
|
164
|
+
const detail: Seg[] = item.description && width >= 60 ? [{ text: ` ${item.description}`, role: "muted" }] : [];
|
|
165
|
+
const hint: Seg[] = item.hint ? [{ text: item.hint, role: "muted" }] : [];
|
|
166
|
+
if (!hint.length) return [...lead, label, ...detail];
|
|
167
|
+
const room = Math.max(4, width - segsWidth(hint) - 1);
|
|
168
|
+
const left = [...lead, label, ...detail];
|
|
169
|
+
const fitted = segsWidth(left) <= room ? left : [...lead, { ...label, text: clipText(label.text, Math.max(1, room - segsWidth(lead))) }];
|
|
170
|
+
return splitLine(width, [{ segs: fitted, priority: 0 }], [{ segs: hint, priority: 1 }], 1);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The empty-session greeting (docs/tasks/refactor-tui-dock-layout.md). Pure.
|
|
3
|
+
*
|
|
4
|
+
* ▝▖ ▗▘ xeno 0.2.1
|
|
5
|
+
* ▐█▙█▟█▌ DeepSeek V4 Pro · high
|
|
6
|
+
* ▀▘ ▝▀ ~/Projects/workspace/xeno-tui ⎇ main
|
|
7
|
+
*
|
|
8
|
+
* An original 7×3 glyph (xeno: the stranger) drawn with ordinary quadrant
|
|
9
|
+
* blocks, so no Nerd Font is needed. Its entrance plays once: antennae rise,
|
|
10
|
+
* wiggle, one blink, then rest. Every frame keeps the same cells, so the
|
|
11
|
+
* terminal never reflows and a selection is never disturbed. Claude Code's
|
|
12
|
+
* fullscreen welcome plays a 0.4–1.3 s entrance the same way; Codex and
|
|
13
|
+
* OpenCode both dropped animations that kept running.
|
|
14
|
+
*/
|
|
15
|
+
import type { Line } from "./status.ts";
|
|
16
|
+
|
|
17
|
+
export const MASCOT_WIDTH = 7;
|
|
18
|
+
|
|
19
|
+
const ANTENNAE_UP = "▝▖ ▗▘";
|
|
20
|
+
const ANTENNAE_IN = "▗▘ ▝▖";
|
|
21
|
+
const ANTENNAE_DOWN = " ▖ ▗ ";
|
|
22
|
+
const EYES_OPEN = "▐█▙█▟█▌";
|
|
23
|
+
const EYES_SHUT = "▐█████▌";
|
|
24
|
+
const FEET = " ▀▘ ▝▀ ";
|
|
25
|
+
|
|
26
|
+
/** The resting pose: what a still terminal and every frame after the entrance show. */
|
|
27
|
+
export const MASCOT: readonly string[] = [ANTENNAE_UP, EYES_OPEN, FEET];
|
|
28
|
+
|
|
29
|
+
/** 60 ms per frame: about 16 fps, smooth for blocky glyphs without a frame per paint. Assumed. */
|
|
30
|
+
export const MASCOT_FRAME_MS = 60;
|
|
31
|
+
|
|
32
|
+
/** Asleep, antennae rise, eyes open, wiggle, wiggle, blink, rest. 14 frames = 840 ms, under the 1 s the card allows. */
|
|
33
|
+
const ENTRANCE: readonly (readonly [string, string])[] = [
|
|
34
|
+
[ANTENNAE_DOWN, EYES_SHUT], [ANTENNAE_DOWN, EYES_SHUT], [ANTENNAE_UP, EYES_SHUT], [ANTENNAE_UP, EYES_OPEN],
|
|
35
|
+
[ANTENNAE_IN, EYES_OPEN], [ANTENNAE_UP, EYES_OPEN], [ANTENNAE_IN, EYES_OPEN], [ANTENNAE_UP, EYES_OPEN],
|
|
36
|
+
[ANTENNAE_UP, EYES_OPEN], [ANTENNAE_UP, EYES_SHUT], [ANTENNAE_UP, EYES_OPEN], [ANTENNAE_UP, EYES_OPEN],
|
|
37
|
+
[ANTENNAE_UP, EYES_OPEN], [ANTENNAE_UP, EYES_OPEN],
|
|
38
|
+
];
|
|
39
|
+
export const MASCOT_FRAMES = ENTRANCE.length;
|
|
40
|
+
|
|
41
|
+
/** Frame `n` of the entrance; anything past the end is the resting pose. */
|
|
42
|
+
export function mascotFrame(n: number): readonly string[] {
|
|
43
|
+
const pose = ENTRANCE[Math.max(0, Math.floor(n))];
|
|
44
|
+
return pose ? [pose[0], pose[1], FEET] : MASCOT;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface Greeting {
|
|
48
|
+
readonly version: string;
|
|
49
|
+
/** Model display name and thinking level, or null when no provider can authenticate. */
|
|
50
|
+
readonly model: string | null;
|
|
51
|
+
readonly thinking: string;
|
|
52
|
+
readonly workspace: string;
|
|
53
|
+
readonly branch: string | null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Three rows: mascot frame, a two-cell gap, then version / model / location. */
|
|
57
|
+
export function greetingLines(greeting: Greeting, frame: readonly string[] = MASCOT): Line[] {
|
|
58
|
+
const art = (row: number) => ({ text: frame[row] ?? "", role: "accent" as const });
|
|
59
|
+
const gap = { text: " ", role: "text" as const };
|
|
60
|
+
return [
|
|
61
|
+
[art(0), gap, { text: "xeno", role: "text", bold: true }, { text: ` ${greeting.version}`, role: "muted" }],
|
|
62
|
+
[art(1), gap, ...(greeting.model
|
|
63
|
+
? [{ text: greeting.model, role: "text" as const }, { text: ` · ${greeting.thinking}`, role: "muted" as const }]
|
|
64
|
+
: [{ text: "No provider connected yet · /login", role: "warning" as const }])],
|
|
65
|
+
[art(2), gap, { text: greeting.workspace, role: "muted" }, ...(greeting.branch ? [{ text: ` ⎇ ${greeting.branch}`, role: "muted" as const }] : [])],
|
|
66
|
+
];
|
|
67
|
+
}
|