@visual-z/xeno 0.5.0 → 0.7.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 +19 -0
- package/README.md +19 -4
- package/docs/app-server.md +32 -3
- package/docs/foundations.md +88 -0
- package/docs/install.md +23 -2
- package/docs/research/foundation-clients-2026-10-09.md +27 -0
- package/docs/research/foundation-editing-2026-10-09.md +56 -0
- package/docs/research/foundation-network-2026-10-09.md +37 -0
- package/docs/research/foundation-workflow-2026-10-09.md +37 -0
- package/package.json +10 -2
- package/src/apps/acp-server.ts +41 -0
- package/src/apps/agent-profiles.ts +19 -0
- package/src/apps/app-server.ts +4 -2
- package/src/apps/cli.ts +18 -2
- package/src/apps/commands.ts +20 -0
- package/src/apps/control-transport.ts +3 -2
- package/src/apps/model-catalog.ts +57 -31
- package/src/apps/question-ui.ts +27 -0
- package/src/apps/resources.ts +63 -25
- package/src/apps/review.ts +109 -0
- package/src/apps/session-hub.ts +95 -0
- package/src/apps/shell.ts +55 -1
- package/src/apps/startup.ts +157 -22
- package/src/apps/ui/app.ts +89 -38
- package/src/apps/ui/image-cache.ts +71 -28
- package/src/apps/ui/surface.ts +1 -0
- package/src/apps/update-check.ts +71 -0
- package/src/apps/workflow.ts +58 -0
- package/src/core/activity-status.ts +28 -0
- package/src/core/agent-profile.ts +130 -0
- package/src/core/control-protocol.ts +8 -1
- package/src/core/file-precondition.ts +6 -0
- package/src/core/git-status.ts +29 -0
- package/src/core/image-limits.ts +72 -0
- package/src/core/lsp-framing.ts +34 -0
- package/src/core/mcp-config.ts +68 -0
- package/src/core/patch.ts +130 -0
- package/src/core/permissions.ts +143 -4
- package/src/core/progress.ts +9 -0
- package/src/core/question.ts +94 -0
- package/src/core/request-budget.ts +13 -0
- package/src/core/review.ts +68 -0
- package/src/core/session-exit.ts +26 -0
- package/src/core/settings.ts +1 -1
- package/src/core/stall.ts +18 -6
- package/src/core/stop-report.ts +25 -0
- package/src/core/update-check.ts +24 -5
- package/src/core/web-content.ts +80 -0
- package/src/core/workflow-mode.ts +34 -0
- package/src/runtime/acp-agent.ts +289 -0
- package/src/runtime/agent-profiles.ts +21 -0
- package/src/runtime/catalog.ts +2 -2
- package/src/runtime/control-client.ts +149 -0
- package/src/runtime/control-connection.ts +39 -4
- package/src/runtime/exec-sessions.ts +40 -4
- package/src/runtime/extensions.ts +20 -2
- package/src/runtime/formatter-tool.ts +38 -0
- package/src/runtime/formatter.ts +50 -0
- package/src/runtime/foundation.ts +78 -0
- package/src/runtime/images.ts +6 -2
- package/src/runtime/integrations.ts +8 -2
- package/src/runtime/loop.ts +124 -21
- package/src/runtime/lsp-client.ts +201 -0
- package/src/runtime/mcp-client.ts +264 -0
- package/src/runtime/mcp-oauth.ts +168 -0
- package/src/runtime/mcp-schema.ts +51 -0
- package/src/runtime/oauth.ts +1 -1
- package/src/runtime/openai-responses.ts +6 -4
- package/src/runtime/openai.ts +6 -1
- package/src/runtime/output-schema.ts +26 -0
- package/src/runtime/patch-tool.ts +38 -0
- package/src/runtime/permission-resources.ts +11 -0
- package/src/runtime/pi-provider.ts +5 -4
- package/src/runtime/provider-tool-policy.ts +33 -0
- package/src/runtime/provider.ts +8 -1
- package/src/runtime/question-tool.ts +62 -0
- package/src/runtime/review-tool.ts +48 -0
- package/src/runtime/session-control.ts +61 -2
- package/src/runtime/session-hub.ts +85 -0
- package/src/runtime/tool-discovery.ts +4 -3
- package/src/runtime/tools.ts +44 -10
- package/src/runtime/update-check.ts +3 -2
- package/src/runtime/web-http.ts +56 -0
- package/src/runtime/web-tools.ts +88 -0
- package/src/runtime/workspace-lifecycle.ts +14 -1
- package/src/store/agent-profile-files.ts +13 -0
- package/src/store/blobs.ts +2 -1
- package/src/store/context-files.ts +77 -1
- package/src/store/file-checkpoints.ts +5 -4
- package/src/store/foundation-config.ts +24 -0
- package/src/store/git-status.ts +15 -0
- package/src/store/lsp-document.ts +30 -0
- package/src/store/mcp-credentials.ts +85 -0
- package/src/store/patch.ts +92 -0
- package/src/store/permission-paths.ts +34 -0
- package/src/store/review.ts +109 -0
- package/src/store/search.ts +48 -9
- package/src/tui/ui/layout.ts +7 -7
- package/src/tui/ui/messages.ts +1 -1
- package/src/tui/ui/pictures.ts +26 -9
- package/src/tui/ui/screen.ts +12 -8
- package/src/tui/ui/status.ts +32 -6
package/src/tui/ui/screen.ts
CHANGED
|
@@ -31,7 +31,7 @@ import { screenLayout, sidebarContentWidth, type ScreenLayout } from "./layout.t
|
|
|
31
31
|
import { greetingLines, mascotFrame, MASCOT_FRAME_MS, MASCOT_FRAMES, type Greeting } from "./mascot.ts";
|
|
32
32
|
import { MessageList, type MessageFeatures } from "./messages.ts";
|
|
33
33
|
import { Motion } from "./motion.ts";
|
|
34
|
-
import { contextLabel, sidebarLines, topbarRow, type Line, type StatusModel } from "./status.ts";
|
|
34
|
+
import { contextLabel, modeLabels, sidebarLines, topbarRow, type Line, type StatusModel } from "./status.ts";
|
|
35
35
|
import { DEFAULT_CELL, stripImageRows } from "./image-size.ts";
|
|
36
36
|
import { Pictures, type PictureSource } from "./pictures.ts";
|
|
37
37
|
import type { ImageView, ViewItem } from "./items.ts";
|
|
@@ -112,7 +112,7 @@ export class Screen {
|
|
|
112
112
|
private queue: readonly string[] = [];
|
|
113
113
|
private attachmentImages: readonly ImageView[] = [];
|
|
114
114
|
private attachmentsKey = "";
|
|
115
|
-
private picturesSource: PictureSource = { mode: "chip", protocol: "blocks", cell: DEFAULT_CELL, widthCells: 60, load: async () =>
|
|
115
|
+
private picturesSource: PictureSource = { mode: "chip", protocol: "blocks", cell: DEFAULT_CELL, widthCells: 60, load: async () => ({ kind: "error", reason: "closed" }) };
|
|
116
116
|
private readonly attachments: BoxRenderable;
|
|
117
117
|
private widgets: readonly string[] = [];
|
|
118
118
|
private unread = 0;
|
|
@@ -317,7 +317,7 @@ export class Screen {
|
|
|
317
317
|
|
|
318
318
|
private paintStatus(): void {
|
|
319
319
|
const model = this.status;
|
|
320
|
-
if (model && this.topbar.visible) this.topbar.setRows([topbarRow(model, Math.max(1, this.renderer.width - 2))]);
|
|
320
|
+
if (model && this.topbar.visible) this.topbar.setRows([topbarRow(this.shape.tier === "sidebar" ? { ...model, title: null } : model, Math.max(1, this.renderer.width - 2))]);
|
|
321
321
|
if (this.controlShown) {
|
|
322
322
|
this.sidebar.borderColor = this.controlFocused ? this.colors.accent : this.colors.line;
|
|
323
323
|
this.sidebarRows.setRows(this.controlView.rows(this.sidebarRows.width, this.sidebarRows.height, this.controlFocused));
|
|
@@ -327,7 +327,7 @@ export class Screen {
|
|
|
327
327
|
if (!model) { this.sidebarRows.setRows([]); return; }
|
|
328
328
|
if (this.sidebar.visible) {
|
|
329
329
|
const width = Math.max(1, this.shape.sidebarWidth - 3);
|
|
330
|
-
this.sidebarRows.setRows(sidebarLines(model, width, Math.max(0, this.renderer.height - 1)));
|
|
330
|
+
this.sidebarRows.setRows(sidebarLines(model, width, Math.max(0, this.renderer.height - (this.shape.topbar ? 1 : 0) - 1)));
|
|
331
331
|
}
|
|
332
332
|
}
|
|
333
333
|
|
|
@@ -361,6 +361,7 @@ export class Screen {
|
|
|
361
361
|
let left: Label[] = [];
|
|
362
362
|
if (dialog) {
|
|
363
363
|
left = [{ segs: [{ text: this.dialogs.title, role: "text", bold: true }], priority: 0, min: 8 }];
|
|
364
|
+
if (this.status?.activity?.state === "blocked") right.push({ segs: [{ text: this.status.activity.label, role: "warning" }], priority: 1, min: 8 });
|
|
364
365
|
if (this.dialogs.edgeHint) right.push({ segs: [{ text: this.dialogs.edgeHint, role: "muted" }], priority: 3, min: 10 });
|
|
365
366
|
} else if (this.controlShown && this.renderer.height <= 3) {
|
|
366
367
|
// No transcript rows remain for a drawer. Keep its state and explain how
|
|
@@ -368,19 +369,22 @@ export class Screen {
|
|
|
368
369
|
left = [{ segs: [{ text: "Resize for control", role: "accent" }], priority: 0, min: 1 }];
|
|
369
370
|
} else if (d.busy) {
|
|
370
371
|
const seconds = Math.max(0, Math.floor((Date.now() - this.busySince) / 1000));
|
|
371
|
-
|
|
372
|
+
const activity = this.status?.activity;
|
|
373
|
+
const label = activity ? `${activity.label}${activity.label === "Running tool" || activity.label === "Retrying" ? ` · ${activity.detail}` : ""}${activity.queued ? ` · ${activity.queued} queued` : ""}` : d.activity;
|
|
374
|
+
left = [{ segs: [{ text: "●", role: "accent", spin: activity?.state !== "blocked" }, { text: ` ${label}`, role: activity?.state === "blocked" ? "warning" : "muted" }, { text: ` ${seconds}s`, role: "muted" }], priority: 0, min: 1, shrink: 1 }];
|
|
372
375
|
right.push({ segs: [{ text: "esc interrupt", role: "muted" }], priority: 4 });
|
|
373
376
|
} else {
|
|
374
377
|
// A draft taller than the box says what scrolled out of view (fix-tui-composer-growth DoD 4).
|
|
375
378
|
const { above, below } = this.composer.overflow;
|
|
376
379
|
if (above) left = [{ segs: [{ text: `↑ ${above} more`, role: "muted" }], priority: 1 }];
|
|
377
380
|
if (below && !this.toastText) right.push({ segs: [{ text: `↓ ${below} more`, role: "muted" }], priority: 2 });
|
|
381
|
+
if (!above && this.status?.activity) left = [{ segs: [{ text: this.status.activity.label, role: this.status.activity.state === "error" ? "error" : this.status.activity.state === "blocked" ? "warning" : "muted" }], priority: 1, min: 4 }];
|
|
378
382
|
}
|
|
379
383
|
this.topEdge.setRows([edgeLine({ width, corners: ["╭", "╮"], role, left, right })]);
|
|
380
|
-
const bottomLeft: Label[] = d.model ? [
|
|
381
|
-
{ segs: [{ text: d.model, role: "text", bold: true }], priority:
|
|
384
|
+
const bottomLeft: Label[] = [...(this.status ? modeLabels(this.status) : []), ...(d.model ? [
|
|
385
|
+
{ segs: [{ text: d.model, role: "text" as const, bold: true }], priority: 1, min: 8 },
|
|
382
386
|
...(d.thinking ? [{ segs: [{ text: d.thinking, role: "muted" as const }], priority: 2 }] : []),
|
|
383
|
-
] : [];
|
|
387
|
+
] : [])];
|
|
384
388
|
const context = this.shape.tier !== "sidebar" && this.status ? contextLabel(this.status) : undefined;
|
|
385
389
|
this.bottomEdge.setRows([edgeLine({ width, corners: ["╰", "╯"], role, left: bottomLeft, right: context ? [context] : [] })]);
|
|
386
390
|
}
|
package/src/tui/ui/status.ts
CHANGED
|
@@ -34,6 +34,10 @@ export interface StatusModel {
|
|
|
34
34
|
readonly workspace: string;
|
|
35
35
|
readonly workspaceShort: string;
|
|
36
36
|
readonly branch: string | null;
|
|
37
|
+
readonly mode?: { readonly yolo: boolean; readonly workflow: "build" | "plan" };
|
|
38
|
+
readonly provider?: { readonly name: string; readonly api: string; readonly host: string | null; readonly role: string | null; readonly ready: boolean };
|
|
39
|
+
readonly activity?: { readonly state: string; readonly label: string; readonly detail: string; readonly queued: number };
|
|
40
|
+
readonly git?: { readonly staged: number; readonly changed: number; readonly untracked: number; readonly conflicted: number; readonly ahead: number; readonly behind: number } | null;
|
|
37
41
|
readonly context?: ContextStatus;
|
|
38
42
|
readonly usage?: { readonly input: number; readonly output: number; readonly cacheRate: number; readonly cost: string | null };
|
|
39
43
|
readonly tools: { readonly running: number; readonly done: number; readonly failed: number };
|
|
@@ -71,10 +75,18 @@ function shareSeg(context: ContextStatus): Seg | undefined {
|
|
|
71
75
|
return { text: `${context.estimated ? "~" : ""}${Math.round(share * 100)}%`, role: share >= CONTEXT_WARNING ? "warning" : "text" };
|
|
72
76
|
}
|
|
73
77
|
|
|
74
|
-
/** The input box's bottom-right label
|
|
78
|
+
/** The input box's bottom-right label names the measured budget: `seg ~12%` or `model ~12%`. */
|
|
75
79
|
export function contextLabel(model: StatusModel): Label | undefined {
|
|
76
80
|
const share = model.context && shareSeg(model.context);
|
|
77
|
-
return share ? { segs: [{ text: "
|
|
81
|
+
return share ? { segs: [{ text: model.context?.trigger === null ? "model " : "seg ", role: "muted" }, share], priority: 3 } : undefined;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function modeLabels(model: StatusModel): Label[] {
|
|
85
|
+
if (!model.mode) return [];
|
|
86
|
+
return [
|
|
87
|
+
{ segs: [{ text: model.mode.yolo ? "YOLO" : "Approval", role: model.mode.yolo ? "warning" : "muted", bold: model.mode.yolo }], priority: 0 },
|
|
88
|
+
{ segs: [{ text: model.mode.workflow === "plan" ? "Plan" : "Build", role: "accent" }], priority: 0 },
|
|
89
|
+
];
|
|
78
90
|
}
|
|
79
91
|
|
|
80
92
|
export function terminalTitle(model: StatusModel): string {
|
|
@@ -91,6 +103,16 @@ function firstLine(text: string): string { return text.split("\n").find(line =>
|
|
|
91
103
|
* row is narrow: location goes before counts, as OpenCode #570 asked.
|
|
92
104
|
*/
|
|
93
105
|
export function topbarRow(model: StatusModel, width: number): Seg[] {
|
|
106
|
+
if (model.provider) {
|
|
107
|
+
const provider = model.provider;
|
|
108
|
+
return splitLine(width, [
|
|
109
|
+
{ segs: [{ text: "xeno", role: "text", bold: true }], priority: 0 },
|
|
110
|
+
{ segs: [{ text: `${provider.ready ? "" : "! "}${provider.name}`, role: provider.ready ? "text" : "warning" }], priority: 0, min: 6 },
|
|
111
|
+
{ segs: [{ text: provider.api, role: "muted" }], priority: 2, min: 8 },
|
|
112
|
+
...(provider.host ? [{ segs: [{ text: provider.host, role: "muted" as const }], priority: 3, min: 8 }] : []),
|
|
113
|
+
...(provider.role ? [{ segs: [{ text: `agent ${provider.role}`, role: "accent" as const }], priority: 1, min: 8 }] : []),
|
|
114
|
+
], model.title ? [{ segs: [{ text: firstLine(model.title), role: "muted" }], priority: 4, min: 12 }] : []);
|
|
115
|
+
}
|
|
94
116
|
const left: Label[] = [
|
|
95
117
|
{ segs: [{ text: "xeno", role: "text", bold: true }], priority: 0 },
|
|
96
118
|
// Never dropped, but gives up width after cost and background counts, before the handoff counts go.
|
|
@@ -175,6 +197,9 @@ function sidebarBlocks(model: StatusModel, w: number): Block[] {
|
|
|
175
197
|
...title.map(text => [{ text, role: model.title ? "text" : "muted", bold: Boolean(model.title) }] as Line),
|
|
176
198
|
[{ text: clipText(model.workspace, w), role: "muted" }],
|
|
177
199
|
...(model.branch ? [[{ text: "⎇ ", role: "muted" }, { text: model.branch, role: "muted" }] as Line] : []),
|
|
200
|
+
...(model.git ? [[{ text: `${model.git.staged} staged · ${model.git.changed} changed · ${model.git.untracked} new`, role: "muted" }] as Line,
|
|
201
|
+
...(model.git.conflicted ? [[{ text: `${model.git.conflicted} conflicts`, role: "warning" }] as Line] : []),
|
|
202
|
+
...(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] : []),
|
|
178
203
|
] });
|
|
179
204
|
const context = model.context;
|
|
180
205
|
if (model.goal) blocks.push({ list: false, items: [], head: [
|
|
@@ -190,10 +215,11 @@ function sidebarBlocks(model: StatusModel, w: number): Block[] {
|
|
|
190
215
|
const tone = share !== undefined && share >= CONTEXT_WARNING ? "warning" : "accent";
|
|
191
216
|
const shareText = shareSeg(context);
|
|
192
217
|
blocks.push({ list: false, items: [], head: [
|
|
193
|
-
row([{ text: "Context", role: "text", bold: true }], shareText ? [shareText] : []),
|
|
218
|
+
row([{ text: "Context", role: "text", bold: true }], shareText ? [{ text: context.trigger === null ? "model " : "seg ", role: "muted" }, shareText] : []),
|
|
194
219
|
...(share === undefined ? [] : [[{ text: "━".repeat(filled), role: tone }, { text: "─".repeat(Math.max(0, w - filled)), role: "line" }] as Line]),
|
|
195
|
-
[{ text:
|
|
196
|
-
|
|
220
|
+
[{ text: `Segment ${est}${compact(context.used)}${context.trigger !== null ? ` / ${est}${compact(context.trigger)}` : " · cut disabled"}`, role: "text" }],
|
|
221
|
+
[{ text: context.window ? `Model ${est}${compact(context.used)} / ${compact(context.window)} · ${est}${Math.round(context.used / context.window * 100)}%` : "Model window unknown", role: "muted" }],
|
|
222
|
+
[{ text: `node ${context.segment}`, role: "muted" }],
|
|
197
223
|
] });
|
|
198
224
|
}
|
|
199
225
|
const usage = model.usage, tools = model.tools;
|
|
@@ -222,7 +248,7 @@ function sidebarBlocks(model: StatusModel, w: number): Block[] {
|
|
|
222
248
|
blocks.push({ list: true, head: [row([{ text: "Tasks", role: "text", bold: true }], [{ text: `${tasks.length} open`, role: "muted" }])],
|
|
223
249
|
items: tasks.map(task => [{ text: task.status === "blocked" ? "! " : "◐ ", role: task.status === "blocked" ? "warning" : "accent" }, { text: task.what, role: "text" }] as Line) });
|
|
224
250
|
const files = model.files ?? [];
|
|
225
|
-
blocks.push({ list: true, head: [row([{ text: "
|
|
251
|
+
blocks.push({ list: true, head: [row([{ text: "Agent files", role: "text", bold: true }], [{ text: String(files.length), role: "muted" }])],
|
|
226
252
|
items: files.map(file => fileLine(file, w)) });
|
|
227
253
|
const background = model.background ?? [];
|
|
228
254
|
blocks.push({ list: true, head: [row([{ text: "Background", role: "text", bold: true }], [{ text: String(background.length), role: "muted" }])],
|