@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
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import type { ControlSnapshot } from "../../runtime/session-control.ts";
|
|
2
|
-
import type { ControlPanelModel } from "../../tui/ui/control-panel.ts";
|
|
3
|
-
import type { RequestState } from "../../core/request-state.ts";
|
|
4
|
-
import { evidenceLadder, ladderGlyph, RUNGS } from "../../core/evidence-ladder.ts";
|
|
5
|
-
|
|
6
|
-
/** The sidebar's compact ladder: rung names with their glyphs, then the reading. */
|
|
7
|
-
export function ladderView(state: RequestState): { ladder?: { glyphs: string; summary: string } } {
|
|
8
|
-
const ladder = evidenceLadder(state);
|
|
9
|
-
if (!ladder) return {};
|
|
10
|
-
const names = { ran: "ran", discriminating: "tests", independent: "blind", human: "human" } as const;
|
|
11
|
-
return { ladder: { glyphs: RUNGS.map(rung => `${names[rung]} ${ladderGlyph(ladder[rung])}`).join(" "), summary: ladder.summary } };
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export function controlModel(snapshot: ControlSnapshot): ControlPanelModel {
|
|
15
|
-
const goal = snapshot.goal, state = goal?.state, context = snapshot.context;
|
|
16
|
-
const progress = (label: string, sources: readonly { node: number; event: string }[]) => ({ label, count: sources.length, sources: sources.map(source => `${source.node}/${source.event}`) });
|
|
17
|
-
return { thread: snapshot.threadId, turn: snapshot.activeTurnId ?? state?.request.event ?? null,
|
|
18
|
-
goal: goal?.text ?? "Send a request to establish a goal.", status: state?.status ?? "idle",
|
|
19
|
-
execution: snapshot.waiting ? `waiting for ${snapshot.waiting}` : snapshot.activeTurnId ? "running" : state?.execution?.reason ?? "idle",
|
|
20
|
-
gaps: [...ladderLines(state), ...goal?.gaps ?? []], checks: state?.commands.map(command => `${command.status} · exit ${command.exit} · ${command.command} · ${command.source.node}/${command.source.event}`) ?? [],
|
|
21
|
-
progress: state ? [
|
|
22
|
-
progress("New observations", state.progress.observations),
|
|
23
|
-
progress("Artifact changes", state.progress.artifact_changes),
|
|
24
|
-
progress("Acceptance advances", state.progress.acceptance_advances),
|
|
25
|
-
progress("New diagnostics", state.progress.new_diagnostics),
|
|
26
|
-
progress("Unchanged retries", state.progress.unchanged_retries),
|
|
27
|
-
progress("Unknown diagnostics", state.progress.unknown_diagnostics),
|
|
28
|
-
] : [],
|
|
29
|
-
context: [`${context.used} tokens (${context.countKind}); ${context.messages.length} messages; ${context.policy}`,
|
|
30
|
-
...Object.entries(context.components).map(([name, count]) => `${name}: ~${count}`),
|
|
31
|
-
...context.messages.map(message => `#${message.index} ${message.role}: ${message.sources.join(", ")}`)],
|
|
32
|
-
omitted: context.omitted.map(event => `${event.id}: ${event.type} (original stays in log)`),
|
|
33
|
-
history: context.sections.filter(section => section.total > 0).map(section => `${section.section}: ${section.retained}/${section.total}` +
|
|
34
|
-
(section.recovery ? ` · history(${JSON.stringify(section.recovery)})` : "")),
|
|
35
|
-
};
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/** The control panel's goal page spells each rung out on its own line. */
|
|
39
|
-
function ladderLines(state: RequestState | undefined): string[] {
|
|
40
|
-
const ladder = state ? evidenceLadder(state) : null;
|
|
41
|
-
if (!ladder) return [];
|
|
42
|
-
return [`Evidence: ${ladder.summary}`, ...RUNGS.map(rung => ` ${ladderGlyph(ladder[rung])} ${rung}: ${ladder[rung]}`), ""];
|
|
43
|
-
}
|
|
@@ -1,194 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ExtensionUI for the interactive view. Dialog requests are serialized (one
|
|
3
|
-
* extension question at a time, in arrival order) and all cancel on
|
|
4
|
-
* shutdown or session switch, resolving as "dismissed". A call arriving from
|
|
5
|
-
* a disposed generation (an extension of the previous session finishing
|
|
6
|
-
* late) is ignored rather than mutating the new session's UI.
|
|
7
|
-
*/
|
|
8
|
-
import type { Theme } from "../../core/theme.ts";
|
|
9
|
-
import type { DialogOptions, ExtensionComponent, ExtensionTheme, ExtensionUI } from "../../runtime/extension-ui.ts";
|
|
10
|
-
import type { ExtensionRegistry, ExtensionToolResult } from "../../runtime/extensions.ts";
|
|
11
|
-
import type { DialogSpec } from "../../tui/ui/dialog.ts";
|
|
12
|
-
|
|
13
|
-
export interface ExtensionHostDeps {
|
|
14
|
-
dialog(spec: DialogSpec): void;
|
|
15
|
-
closeDialog(spec: DialogSpec): void;
|
|
16
|
-
notify(message: string, tone: "info" | "warning" | "error"): void;
|
|
17
|
-
redraw(): void;
|
|
18
|
-
theme(): Theme;
|
|
19
|
-
colored(): boolean;
|
|
20
|
-
editorText(): string;
|
|
21
|
-
setEditorText(text: string): void;
|
|
22
|
-
insertEditorText(text: string): void;
|
|
23
|
-
toolsExpanded(): boolean;
|
|
24
|
-
setToolsExpanded(value: boolean): void;
|
|
25
|
-
setTitle(title: string): void;
|
|
26
|
-
cwd(): string;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export class ExtensionHost {
|
|
30
|
-
private readonly deps: ExtensionHostDeps;
|
|
31
|
-
private generation = 0;
|
|
32
|
-
private queue: Promise<unknown> = Promise.resolve();
|
|
33
|
-
private readonly pending = new Set<() => void>();
|
|
34
|
-
private readonly statuses = new Map<string, string>();
|
|
35
|
-
private readonly widgets = new Map<string, string[]>();
|
|
36
|
-
private readonly toolState = new Map<string, Record<string, unknown>>();
|
|
37
|
-
private readonly partials = new Map<string, ExtensionToolResult>();
|
|
38
|
-
private registry: ExtensionRegistry | undefined;
|
|
39
|
-
private unwatch: (() => void) | undefined;
|
|
40
|
-
workingMessage: string | undefined;
|
|
41
|
-
hiddenThinkingLabel: string | undefined;
|
|
42
|
-
private api: ExtensionUI;
|
|
43
|
-
|
|
44
|
-
constructor(deps: ExtensionHostDeps) {
|
|
45
|
-
this.deps = deps;
|
|
46
|
-
this.api = this.createApi();
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
get context(): ExtensionUI { return this.api; }
|
|
50
|
-
get status(): string { return [...this.statuses.values()].join(" · "); }
|
|
51
|
-
get widgetLines(): string[] { return [...this.widgets.values()].flat(); }
|
|
52
|
-
|
|
53
|
-
bind(registry: ExtensionRegistry | undefined): void {
|
|
54
|
-
if (registry === this.registry && this.unwatch) return;
|
|
55
|
-
if (registry !== this.registry) this.reset();
|
|
56
|
-
this.registry = registry;
|
|
57
|
-
this.unwatch = registry?.onToolUpdate((_name, id, _input, result) => { this.partials.set(id, result); this.deps.redraw(); });
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/** New generation: cancel dialogs, drop widgets and statuses. Late calls from the old one become no-ops. */
|
|
61
|
-
reset(): void {
|
|
62
|
-
this.generation++;
|
|
63
|
-
this.cancelDialogs();
|
|
64
|
-
this.unwatch?.(); this.unwatch = undefined; this.registry = undefined;
|
|
65
|
-
this.statuses.clear(); this.widgets.clear(); this.toolState.clear(); this.partials.clear();
|
|
66
|
-
this.workingMessage = undefined; this.hiddenThinkingLabel = undefined;
|
|
67
|
-
this.api = this.createApi();
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
cancelDialogs(): void { for (const cancel of [...this.pending]) cancel(); }
|
|
71
|
-
|
|
72
|
-
partial(id: string): ExtensionToolResult | undefined { return this.partials.get(id); }
|
|
73
|
-
clearPartials(): void { this.partials.clear(); }
|
|
74
|
-
|
|
75
|
-
theme(): ExtensionTheme {
|
|
76
|
-
const theme = this.deps.theme();
|
|
77
|
-
const colored = this.deps.colored();
|
|
78
|
-
const sgr = (color: string) => colored && theme.colors[color] ? `\x1b[${theme.colors[color]}m` : "";
|
|
79
|
-
const wrap = (open: string, text: string, close: string) => open ? open + text + close : text;
|
|
80
|
-
const attr = (on: number, off: number) => (text: string) => wrap(colored ? `\x1b[${on}m` : "", text, `\x1b[${off}m`);
|
|
81
|
-
return { ...theme, fg: (color, text) => wrap(sgr(color), text, "\x1b[39m"), bg: (color, text) => wrap(sgr(color), text, "\x1b[49m"),
|
|
82
|
-
bold: attr(1, 22), dim: attr(2, 22), italic: attr(3, 23), underline: attr(4, 24), inverse: attr(7, 27), strikethrough: attr(9, 29) };
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/** Render an extension component safely: a throwing renderer shows its error, not a crash. */
|
|
86
|
-
lines(component: ExtensionComponent | undefined, width: number): string[] {
|
|
87
|
-
if (!component) return [];
|
|
88
|
-
try { return component.render(width); } catch (error) { this.deps.notify(`Extension renderer: ${String(error)}`, "error"); return [`(extension renderer failed: ${String(error)})`]; }
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
toolRenderer(name: string, input: unknown, result: { text: string; ok: boolean; data?: unknown } | null, id: string): ((width: number) => string[]) | undefined {
|
|
92
|
-
const tool = this.registry?.tools.get(name);
|
|
93
|
-
if (!tool?.renderCall && !tool?.renderResult) return undefined;
|
|
94
|
-
const state = this.toolState.get(id) ?? {};
|
|
95
|
-
this.toolState.set(id, state);
|
|
96
|
-
const partial = this.partials.get(id);
|
|
97
|
-
const expanded = this.deps.toolsExpanded();
|
|
98
|
-
return width => {
|
|
99
|
-
const context = { args: input, toolCallId: id, invalidate: () => this.deps.redraw(), lastComponent: undefined, state, cwd: this.deps.cwd(),
|
|
100
|
-
executionStarted: result !== null || partial !== undefined, argsComplete: true, isPartial: result === null, expanded, showImages: false, isError: result?.ok === false };
|
|
101
|
-
const call = tool.renderCall ? this.lines(tool.renderCall(input, this.theme(), context), width) : [];
|
|
102
|
-
const payload: ExtensionToolResult | undefined = result
|
|
103
|
-
? resultPayload(result)
|
|
104
|
-
: partial;
|
|
105
|
-
const body = payload && tool.renderResult ? this.lines(tool.renderResult(payload, { expanded, isPartial: result === null }, this.theme(), context), width) : [];
|
|
106
|
-
return [...call, ...body];
|
|
107
|
-
};
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
/** Extension Markdown transformers, in registration order; a throwing one is skipped. Display only. */
|
|
111
|
-
transformMarkdown(text: string, context: { streaming: boolean; messageType: "assistant" | "assistant-thinking"; width: number }): string {
|
|
112
|
-
for (const transform of this.registry?.markdown ?? []) {
|
|
113
|
-
// Context keys are the ones extensions already read (availableWidth / isStreaming / messageType).
|
|
114
|
-
try { text = transform(text, { availableWidth: context.width, isStreaming: context.streaming, messageType: context.messageType }) ?? text; } catch (error) { this.deps.notify(`Markdown transformer: ${String(error)}`, "error"); }
|
|
115
|
-
}
|
|
116
|
-
return text;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
messageRenderer(type: string, text: string, data: unknown, entry: boolean): ((width: number) => string[]) | undefined {
|
|
120
|
-
const renderer = (entry ? this.registry?.entryRenderers.get(type) : undefined) ?? this.registry?.renderers.get(type);
|
|
121
|
-
if (!renderer) return undefined;
|
|
122
|
-
return width => {
|
|
123
|
-
try {
|
|
124
|
-
const component = renderer({ customType: type, content: text, ...(data === undefined ? {} : { details: data }) }, { expanded: this.deps.toolsExpanded(), outputPad: 1 }, this.theme());
|
|
125
|
-
return component ? this.lines(component, width) : [text];
|
|
126
|
-
} catch (error) { return [`(extension renderer failed: ${String(error)})`, text]; }
|
|
127
|
-
};
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
private createApi(): ExtensionUI {
|
|
131
|
-
const generation = this.generation;
|
|
132
|
-
const live = () => generation === this.generation;
|
|
133
|
-
const api: ExtensionUI = {
|
|
134
|
-
select: (title, options, opts) => this.ask<string | undefined>(undefined, opts, resolve => ({
|
|
135
|
-
kind: "list", title, items: options.map(option => ({ value: option, label: option })), onPick: item => resolve(item.value), onCancel: () => resolve(undefined),
|
|
136
|
-
})),
|
|
137
|
-
confirm: (title, message, opts) => this.ask<boolean>(false, opts, resolve => ({ kind: "confirm", title, message, onAnswer: yes => resolve(yes) })),
|
|
138
|
-
input: (title, placeholder, opts) => this.ask<string | undefined>(undefined, opts, resolve => ({
|
|
139
|
-
kind: "form", title, label: placeholder || "value", onSubmit: value => resolve(value), onCancel: () => resolve(undefined),
|
|
140
|
-
})),
|
|
141
|
-
show: (title, component) => this.ask<void>(undefined, undefined, resolve => ({
|
|
142
|
-
kind: "lines", title, render: width => this.lines(component, width), onCancel: () => { component.dispose?.(); resolve(undefined); },
|
|
143
|
-
})),
|
|
144
|
-
notify: (message, type) => { if (live()) this.deps.notify(message, type ?? "info"); },
|
|
145
|
-
setStatus: (key, text) => { if (!live()) return; if (text === undefined) this.statuses.delete(key); else this.statuses.set(key, text); this.deps.redraw(); },
|
|
146
|
-
setWidget: (key, lines) => { if (!live()) return; if (lines === undefined) this.widgets.delete(key); else this.widgets.set(key, lines); this.deps.redraw(); },
|
|
147
|
-
setWorkingMessage: message => { if (!live()) return; this.workingMessage = message; this.deps.redraw(); },
|
|
148
|
-
setHiddenThinkingLabel: label => { if (!live()) return; this.hiddenThinkingLabel = label; this.deps.redraw(); },
|
|
149
|
-
setTitle: title => { if (live()) this.deps.setTitle(title); },
|
|
150
|
-
getEditorText: () => this.deps.editorText(),
|
|
151
|
-
setEditorText: text => { if (live()) this.deps.setEditorText(text); },
|
|
152
|
-
pasteToEditor: text => { if (live()) this.deps.insertEditorText(text); },
|
|
153
|
-
get theme() { return hostTheme(); },
|
|
154
|
-
getToolsExpanded: () => this.deps.toolsExpanded(),
|
|
155
|
-
setToolsExpanded: value => { if (live()) this.deps.setToolsExpanded(value); },
|
|
156
|
-
};
|
|
157
|
-
const hostTheme = () => this.theme();
|
|
158
|
-
return api;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
/** One extension dialog at a time; signal/timeout/shutdown resolve with `dismissed`. */
|
|
162
|
-
private ask<T>(dismissed: T, opts: DialogOptions | undefined, spec: (resolve: (value: T) => void) => DialogSpec): Promise<T> {
|
|
163
|
-
const generation = this.generation;
|
|
164
|
-
const run = () => new Promise<T>(resolve => {
|
|
165
|
-
if (generation !== this.generation || opts?.signal?.aborted) { resolve(dismissed); return; }
|
|
166
|
-
let done = false, timer: ReturnType<typeof setTimeout> | undefined, shown: DialogSpec | undefined;
|
|
167
|
-
const finish = (value: T, close: boolean) => {
|
|
168
|
-
if (done) return;
|
|
169
|
-
done = true;
|
|
170
|
-
clearTimeout(timer);
|
|
171
|
-
opts?.signal?.removeEventListener("abort", abort);
|
|
172
|
-
this.pending.delete(cancel);
|
|
173
|
-
if (close && shown) this.deps.closeDialog(shown);
|
|
174
|
-
resolve(value);
|
|
175
|
-
};
|
|
176
|
-
const cancel = () => finish(dismissed, true);
|
|
177
|
-
const abort = () => finish(dismissed, true);
|
|
178
|
-
this.pending.add(cancel);
|
|
179
|
-
opts?.signal?.addEventListener("abort", abort, { once: true });
|
|
180
|
-
if (opts?.timeout) timer = setTimeout(abort, opts.timeout);
|
|
181
|
-
shown = spec(value => finish(value, false));
|
|
182
|
-
this.deps.dialog(shown);
|
|
183
|
-
});
|
|
184
|
-
const next = this.queue.then(run, run);
|
|
185
|
-
this.queue = next.catch(() => undefined);
|
|
186
|
-
return next;
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
function resultPayload(result: { text: string; ok: boolean; data?: unknown }): ExtensionToolResult {
|
|
191
|
-
const payload: ExtensionToolResult = { content: [{ type: "text", text: result.text }], isError: !result.ok };
|
|
192
|
-
if (result.data !== undefined) payload.details = result.data as NonNullable<ExtensionToolResult["details"]>;
|
|
193
|
-
return payload;
|
|
194
|
-
}
|
|
@@ -1,121 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Decoded pictures, shared and bounded (docs/tasks/feat-tui-inline-images.md DoD 4).
|
|
3
|
-
*
|
|
4
|
-
* The log stores each image once as a content-addressed blob; this keeps one
|
|
5
|
-
* decoded NativeImage per blob for every node that shows it. Each picture
|
|
6
|
-
* node retains its own reference (OpenTUI counts them), so evicting an entry
|
|
7
|
-
* here only drops the cache's reference: a picture still on screen keeps
|
|
8
|
-
* its pixels, and memory is returned once the last node goes.
|
|
9
|
-
*
|
|
10
|
-
* Bytes come from the blob store, which checks the hash; a missing or
|
|
11
|
-
* corrupt blob, or one that will not decode, has a typed failure so the view
|
|
12
|
-
* retains the caption and shows the actual preparation error.
|
|
13
|
-
*/
|
|
14
|
-
import { NativeImage, imageInfo } from "@opentui/core";
|
|
15
|
-
import { getBlobBytes } from "../../store/blobs.ts";
|
|
16
|
-
import { modelImageDimensions, MAX_IMAGE_SOURCE_BYTES, MAX_IMAGE_SOURCE_PIXELS } from "../../core/image-limits.ts";
|
|
17
|
-
import type { PictureLoad } from "../../tui/ui/pictures.ts";
|
|
18
|
-
|
|
19
|
-
/** Two full previews fit; mounted ImageRenderables retain their own references. */
|
|
20
|
-
export const IMAGE_CACHE_BYTES = 64 * 1024 * 1024;
|
|
21
|
-
/** Bounds failed entries and queued requests as well as successful pixels. */
|
|
22
|
-
export const IMAGE_CACHE_ENTRIES = 128;
|
|
23
|
-
/** Limit simultaneous native allocations; remaining admitted loads wait in the bounded cache. */
|
|
24
|
-
const IMAGE_DECODE_CONCURRENCY = 2;
|
|
25
|
-
interface Entry { readonly result: Promise<PictureLoad>; bytes: number; settled: boolean }
|
|
26
|
-
|
|
27
|
-
export class ImageCache {
|
|
28
|
-
private readonly entries = new Map<string, Entry>();
|
|
29
|
-
private used = 0;
|
|
30
|
-
private active = 0;
|
|
31
|
-
private closed = false;
|
|
32
|
-
private readonly waiting: (() => void)[] = [];
|
|
33
|
-
private readonly blobDir: () => string;
|
|
34
|
-
private readonly budget: number;
|
|
35
|
-
|
|
36
|
-
constructor(blobDir: () => string, budget = IMAGE_CACHE_BYTES) {
|
|
37
|
-
this.blobDir = blobDir;
|
|
38
|
-
this.budget = budget;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/** Bytes of decoded pixels held now (tests). */
|
|
42
|
-
get size(): number { return this.used; }
|
|
43
|
-
get count(): number { return this.entries.size; }
|
|
44
|
-
|
|
45
|
-
async load(sha256: string): Promise<NativeImage | null> {
|
|
46
|
-
const result = await this.loadResult(sha256);
|
|
47
|
-
return result.kind === "ready" ? result.image : null;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
loadResult(sha256: string): Promise<PictureLoad> {
|
|
51
|
-
if (this.closed) return Promise.resolve({ kind: "error", reason: "closed" });
|
|
52
|
-
const hit = this.entries.get(sha256);
|
|
53
|
-
if (hit) {
|
|
54
|
-
// Most recently used goes last; eviction starts from the front.
|
|
55
|
-
this.entries.delete(sha256); this.entries.set(sha256, hit);
|
|
56
|
-
return hit.result;
|
|
57
|
-
}
|
|
58
|
-
if (this.entries.size >= IMAGE_CACHE_ENTRIES) {
|
|
59
|
-
const oldest = [...this.entries].find(([, entry]) => entry.settled);
|
|
60
|
-
if (oldest) this.drop(oldest[0], oldest[1]);
|
|
61
|
-
else return Promise.resolve({ kind: "error", reason: "busy" });
|
|
62
|
-
}
|
|
63
|
-
// Capture the blob owner before awaiting: session switches cannot redirect an in-flight read.
|
|
64
|
-
const entry: Entry = { result: this.decode(this.blobDir(), sha256), bytes: 0, settled: false };
|
|
65
|
-
this.entries.set(sha256, entry);
|
|
66
|
-
void entry.result.then(result => {
|
|
67
|
-
entry.settled = true;
|
|
68
|
-
if (this.entries.get(sha256) !== entry) { if (result.kind === "ready") result.image.dispose(); return; }
|
|
69
|
-
entry.bytes = result.kind === "ready" ? result.image.width * result.image.height * 4 : 0;
|
|
70
|
-
this.used += entry.bytes;
|
|
71
|
-
for (const [sha, old] of this.entries) {
|
|
72
|
-
if (this.used <= this.budget) break;
|
|
73
|
-
if (sha !== sha256 && old.settled) this.drop(sha, old);
|
|
74
|
-
}
|
|
75
|
-
});
|
|
76
|
-
return entry.result;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
private async decode(dir: string, sha: string): Promise<PictureLoad> {
|
|
80
|
-
if (this.active >= IMAGE_DECODE_CONCURRENCY) await new Promise<void>(resolve => this.waiting.push(resolve));
|
|
81
|
-
else this.active++;
|
|
82
|
-
if (this.closed) return { kind: "error", reason: "closed" };
|
|
83
|
-
let native: NativeImage | undefined;
|
|
84
|
-
try {
|
|
85
|
-
let bytes: Uint8Array | null;
|
|
86
|
-
try { bytes = await getBlobBytes(dir, sha, MAX_IMAGE_SOURCE_BYTES); }
|
|
87
|
-
catch (error) { return { kind: "error", reason: error instanceof RangeError ? "limit" : "corrupt" }; }
|
|
88
|
-
if (!bytes) return { kind: "error", reason: "missing" };
|
|
89
|
-
if (bytes.length > MAX_IMAGE_SOURCE_BYTES) return { kind: "error", reason: "limit" };
|
|
90
|
-
const info = imageInfo(bytes);
|
|
91
|
-
if (info.width * info.height > MAX_IMAGE_SOURCE_PIXELS) return { kind: "error", reason: "limit" };
|
|
92
|
-
native = await NativeImage.load(bytes);
|
|
93
|
-
const target = modelImageDimensions(native);
|
|
94
|
-
if (target.width !== native.width || target.height !== native.height) {
|
|
95
|
-
const resized = native.resize(target); native.dispose(); native = resized;
|
|
96
|
-
}
|
|
97
|
-
if (this.closed) return { kind: "error", reason: "closed" };
|
|
98
|
-
if (native.width * native.height * 4 > this.budget) return { kind: "error", reason: "limit" };
|
|
99
|
-
const image = native; native = undefined;
|
|
100
|
-
return { kind: "ready", image };
|
|
101
|
-
} catch { return { kind: "error", reason: "decode" }; }
|
|
102
|
-
finally {
|
|
103
|
-
native?.dispose();
|
|
104
|
-
const next = this.waiting.shift();
|
|
105
|
-
if (next) next(); else this.active--;
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
private drop(sha: string, entry: Entry): void {
|
|
110
|
-
this.entries.delete(sha); this.used -= entry.bytes;
|
|
111
|
-
void entry.result.then(result => { if (result.kind === "ready") result.image.dispose(); });
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
dispose(): void {
|
|
115
|
-
this.closed = true;
|
|
116
|
-
for (const entry of this.entries.values()) if (entry.settled) void entry.result.then(result => { if (result.kind === "ready") result.image.dispose(); });
|
|
117
|
-
this.entries.clear();
|
|
118
|
-
this.used = 0;
|
|
119
|
-
this.waiting.splice(0).forEach(resolve => resolve());
|
|
120
|
-
}
|
|
121
|
-
}
|
package/src/apps/ui/palette.ts
DELETED
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Ctrl+P: every command and action in one searchable list.
|
|
3
|
-
*
|
|
4
|
-
* Sources: the slash-command catalog (COMMANDS, prompt templates, skills,
|
|
5
|
-
* extension commands) plus actions that have no slash spelling (toggle tools,
|
|
6
|
-
* scroll, sidebar) and the ready models. The `/` completion menu reads the
|
|
7
|
-
* same command entries, so a command is discoverable exactly one way
|
|
8
|
-
* (OpenCode's command registry). Pure: the controller supplies state.
|
|
9
|
-
*/
|
|
10
|
-
import type { ListItem } from "../../tui/ui/list-model.ts";
|
|
11
|
-
import { COMMANDS } from "../commands.ts";
|
|
12
|
-
|
|
13
|
-
export interface PaletteAction { readonly id: string; readonly label: string; readonly description: string; readonly section: string; readonly chord?: string; readonly when?: boolean }
|
|
14
|
-
|
|
15
|
-
export interface PaletteInput {
|
|
16
|
-
readonly bindings: Readonly<Record<string, readonly string[]>>;
|
|
17
|
-
readonly busy: boolean;
|
|
18
|
-
readonly extraCommands: readonly { name: string; description: string; section: string }[];
|
|
19
|
-
readonly models: readonly { key: string; label: string; description: string; current: boolean }[];
|
|
20
|
-
readonly toolsExpanded: boolean;
|
|
21
|
-
readonly thinkingExpanded: boolean;
|
|
22
|
-
readonly sidebar: boolean;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/** Which section a built-in command belongs to; unlisted ones go under "Commands". */
|
|
26
|
-
const SECTION: Readonly<Record<string, string>> = {
|
|
27
|
-
model: "Model", "scoped-models": "Model", thinking: "Model", login: "Model", logout: "Model",
|
|
28
|
-
new: "Session", resume: "Session", tree: "Session", fork: "Session", clone: "Session", back: "Session", name: "Session", session: "Session",
|
|
29
|
-
export: "Session", import: "Session", copy: "Session",
|
|
30
|
-
usage: "Inspect", state: "Inspect", context: "Inspect", acceptance: "Inspect", tasks: "Inspect", branches: "Inspect", processes: "Inspect", debug: "Inspect",
|
|
31
|
-
settings: "Configure", reload: "Configure", config: "Configure", trust: "Configure", hotkeys: "Configure", models: "Configure",
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
function chord(bindings: PaletteInput["bindings"], action: string): string | undefined {
|
|
35
|
-
return bindings[action]?.[0];
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export function paletteActions(input: PaletteInput): PaletteAction[] {
|
|
39
|
-
const b = input.bindings;
|
|
40
|
-
const actions: PaletteAction[] = [
|
|
41
|
-
{ id: "action:toggleTools", label: input.toolsExpanded ? "Collapse tool output" : "Expand tool output", description: "all tool cards", section: "View", ...opt(chord(b, "app.toggleTools")) },
|
|
42
|
-
{ id: "action:toggleThinking", label: input.thinkingExpanded ? "Hide thinking" : "Show thinking", description: "reasoning blocks", section: "View", ...opt(chord(b, "app.toggleThinking")) },
|
|
43
|
-
{ id: "action:toggleSidebar", label: input.sidebar ? "Hide sidebar" : "Show sidebar", description: "segment state (≥120 columns)", section: "View", ...opt(chord(b, "app.toggleSidebar")) },
|
|
44
|
-
{ id: "action:appearance", label: "Appearance", description: "dark, light or follow terminal", section: "View" },
|
|
45
|
-
{ id: "action:animations", label: "Animations", description: "enable or reduce motion", section: "View" },
|
|
46
|
-
{ id: "action:bottom", label: "Jump to latest", description: "resume following new output", section: "View", ...opt(chord(b, "app.scroll.bottom")) },
|
|
47
|
-
{ id: "action:top", label: "Jump to start", description: "first message of this session", section: "View", ...opt(chord(b, "app.scroll.top")) },
|
|
48
|
-
{ id: "action:editor", label: "Open draft in $EDITOR", description: "edit the prompt externally", section: "Compose", ...opt(chord(b, "app.externalEditor")) },
|
|
49
|
-
{ id: "action:pasteImage", label: "Paste image from clipboard", description: "attach to the draft", section: "Compose", ...opt(chord(b, "app.clipboard.pasteImage")) },
|
|
50
|
-
{ id: "action:cycleThinking", label: "Cycle thinking level", description: "next supported level", section: "Model", ...opt(chord(b, "app.cycleThinking")) },
|
|
51
|
-
{ id: "action:interrupt", label: "Interrupt", description: "stop the running turn", section: "Run", when: input.busy, ...opt(chord(b, "app.interrupt")) },
|
|
52
|
-
{ id: "action:quit", label: "Quit", description: "exit xeno", section: "Run", ...opt(chord(b, "app.exit")) },
|
|
53
|
-
];
|
|
54
|
-
return actions.filter(action => action.when !== false);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function opt(value: string | undefined): { chord?: string } { return value ? { chord: value } : {}; }
|
|
58
|
-
|
|
59
|
-
export function paletteItems(input: PaletteInput): ListItem[] {
|
|
60
|
-
const items: ListItem[] = [];
|
|
61
|
-
const model = input.models.find(entry => entry.current);
|
|
62
|
-
for (const action of paletteActions(input)) items.push({ value: action.id, label: action.label, description: action.description, section: action.section, ...(action.chord ? { hint: action.chord } : {}) });
|
|
63
|
-
for (const command of COMMANDS) {
|
|
64
|
-
if (command.name === "exit" || command.name === "quit") continue;
|
|
65
|
-
const blocked = input.busy && command.busy === "idle";
|
|
66
|
-
items.push({ value: `command:${command.name}`, label: `/${command.name}`, description: blocked ? `${command.description} · needs an idle session` : command.description,
|
|
67
|
-
section: SECTION[command.name] ?? "Commands", disabled: blocked,
|
|
68
|
-
...(command.name === "model" && model ? { hint: model.label } : command.name === "model" && input.bindings["app.model"]?.[0] ? { hint: input.bindings["app.model"][0] } : {}) });
|
|
69
|
-
}
|
|
70
|
-
for (const command of input.extraCommands) items.push({ value: `command:${command.name}`, label: `/${command.name}`, description: command.description, section: command.section });
|
|
71
|
-
for (const entry of input.models) items.push({ value: `model:${entry.key}`, label: entry.label, description: entry.description, section: "Switch model", keywords: entry.key, disabled: input.busy,
|
|
72
|
-
...(input.busy ? { hint: "after this turn" } : entry.current ? { hint: "current" } : {}) });
|
|
73
|
-
return sortSections(items);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
const ORDER = ["View", "Model", "Session", "Compose", "Inspect", "Configure", "Commands", "Prompts", "Skills", "Extensions", "Run", "Switch model"];
|
|
77
|
-
function sortSections(items: ListItem[]): ListItem[] {
|
|
78
|
-
const rank = (section: string | undefined) => { const at = ORDER.indexOf(section ?? ""); return at < 0 ? ORDER.length : at; };
|
|
79
|
-
return items.map((item, index) => ({ item, index })).sort((a, b) => rank(a.item.section) - rank(b.item.section) || a.index - b.index).map(entry => entry.item);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/** `/` completion: the same command entries, filtered by prefix, ranked by the list model's fuzzy order. */
|
|
83
|
-
export function slashItems(input: PaletteInput, prefix: string): ListItem[] {
|
|
84
|
-
const all = paletteItems(input).filter(item => item.value.startsWith("command:"));
|
|
85
|
-
const q = prefix.toLowerCase();
|
|
86
|
-
return all.filter(item => item.label.slice(1).toLowerCase().startsWith(q)).concat(all.filter(item => !item.label.slice(1).toLowerCase().startsWith(q) && item.label.toLowerCase().includes(q)))
|
|
87
|
-
.map(item => ({ ...item, value: `${item.label} ` }));
|
|
88
|
-
}
|
package/src/apps/ui/project.ts
DELETED
|
@@ -1,127 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Log projection → view items. Pure apart from the renderer callbacks it
|
|
3
|
-
* hands to tool cards, which are themselves pure functions of the log.
|
|
4
|
-
*
|
|
5
|
-
* Inputs are the durable transcript (core/transcript.ts), the UI-only
|
|
6
|
-
* notices anchored to it, and the live (uncommitted) stream. The stream is
|
|
7
|
-
* never history: once the log has the assistant event, the stream is gone
|
|
8
|
-
* and the projected item takes its place (same id scheme, no flash).
|
|
9
|
-
*/
|
|
10
|
-
import type { TranscriptItem } from "../../core/transcript.ts";
|
|
11
|
-
import type { ImageView, ViewItem } from "../../tui/ui/items.ts";
|
|
12
|
-
import type { ImageRef } from "../../core/images.ts";
|
|
13
|
-
import { toolCard, toolDetail, toolPreview, toolSummary } from "../../tui/ui/tool-card.ts";
|
|
14
|
-
import type { Style } from "../../tui/style.ts";
|
|
15
|
-
|
|
16
|
-
export interface Notice { readonly id: string; readonly after: number; readonly item: ViewItem }
|
|
17
|
-
|
|
18
|
-
export interface LiveStream {
|
|
19
|
-
readonly thinking: string;
|
|
20
|
-
readonly text: string;
|
|
21
|
-
readonly tools: readonly { id: string; name: string; arguments: string }[];
|
|
22
|
-
readonly output: string;
|
|
23
|
-
readonly bash: string | null;
|
|
24
|
-
readonly retry?: string;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export interface ProjectOptions {
|
|
28
|
-
readonly notices: readonly Notice[];
|
|
29
|
-
readonly live?: LiveStream;
|
|
30
|
-
/** Extension renderers; return lines to replace the default card, undefined to keep it. */
|
|
31
|
-
readonly customTool?: (item: Extract<TranscriptItem, { kind: "tool" }>) => ((width: number) => string[]) | undefined;
|
|
32
|
-
readonly customMessage?: (item: Extract<TranscriptItem, { kind: "custom" }>) => ((width: number) => string[]) | undefined;
|
|
33
|
-
readonly hiddenThinkingLabel?: string;
|
|
34
|
-
readonly style?: () => Style | undefined;
|
|
35
|
-
/**
|
|
36
|
-
* Views of durable items from earlier paints, by item id. A log item never
|
|
37
|
-
* changes without a new revision, so an equal key means an equal view, and
|
|
38
|
-
* a long session re-projects only what changed (refactor-tui-chrome-cleanup
|
|
39
|
-
* DoD 5). The caller clears it when renderers or labels change.
|
|
40
|
-
*/
|
|
41
|
-
readonly cache?: Map<string, { readonly key: string; readonly view: ViewItem | undefined }>;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/** CutReason → words. Unknown reasons (future log versions) print verbatim. */
|
|
45
|
-
const SEGMENT_REASONS: Readonly<Record<string, string>> = {
|
|
46
|
-
trigger: "context boundary", hard_limit: "context limit", model_checkpoint: "model switch",
|
|
47
|
-
};
|
|
48
|
-
|
|
49
|
-
export function projectItems(items: readonly TranscriptItem[], options: ProjectOptions): ViewItem[] {
|
|
50
|
-
const out: ViewItem[] = [];
|
|
51
|
-
let notice = 0;
|
|
52
|
-
const flush = (upTo: number) => { while (notice < options.notices.length && options.notices[notice]!.after <= upTo) out.push(options.notices[notice++]!.item); };
|
|
53
|
-
flush(0);
|
|
54
|
-
let segment = 1;
|
|
55
|
-
items.forEach((item, index) => {
|
|
56
|
-
// A segment's label depends on how many came before it, so that count is part of its key.
|
|
57
|
-
const key = item.kind === "segment" ? `${item.revision}|${segment}` : item.revision;
|
|
58
|
-
const hit = options.cache?.get(item.id);
|
|
59
|
-
let view: ViewItem | undefined;
|
|
60
|
-
if (hit && hit.key === key) { view = hit.view; if (item.kind === "segment" && item.nextNode === undefined) segment++; }
|
|
61
|
-
else { view = projectOne(item, options, () => ++segment); options.cache?.set(item.id, { key, view }); }
|
|
62
|
-
if (view) out.push(view);
|
|
63
|
-
flush(index + 1);
|
|
64
|
-
});
|
|
65
|
-
while (notice < options.notices.length) out.push(options.notices[notice++]!.item);
|
|
66
|
-
const live = options.live;
|
|
67
|
-
if (live) {
|
|
68
|
-
if (live.bash !== null) out.push({ kind: "tool", id: "live:bash", rev: `${live.bash.length}`, name: "shell", summary: "running", status: "pending", detail: () => live.bash!.split("\n").slice(-40) });
|
|
69
|
-
const logged = new Set(items.flatMap(item => item.kind === "tool" ? [item.callId] : []));
|
|
70
|
-
for (const tool of live.tools) {
|
|
71
|
-
if (logged.has(tool.id)) continue;
|
|
72
|
-
let input: unknown;
|
|
73
|
-
try { input = JSON.parse(tool.arguments) as unknown; } catch { input = {}; }
|
|
74
|
-
out.push({ kind: "tool", id: `live:tool:${tool.id}`, rev: `${tool.arguments.length}`, name: tool.name, summary: toolSummary(tool.name, input) || "preparing…", status: "pending",
|
|
75
|
-
detail: width => toolDetail({ name: tool.name, input, result: null }, width, options.style?.()) });
|
|
76
|
-
}
|
|
77
|
-
if (live.output) out.push({ kind: "lines", id: "live:output", rev: `${live.output.length}`, render: () => live.output.split("\n").slice(-12) });
|
|
78
|
-
if (live.retry) out.push({ kind: "notice", id: "live:retry", rev: live.retry, text: live.retry, tone: "warning" });
|
|
79
|
-
if (live.thinking) out.push({ kind: "thinking", id: "live:thinking", rev: `${live.thinking.length}`, text: live.thinking, streaming: true, ...(options.hiddenThinkingLabel !== undefined ? { label: options.hiddenThinkingLabel } : {}) });
|
|
80
|
-
if (live.text) out.push({ kind: "assistant", id: "live:text", rev: `${live.text.length}`, text: live.text, streaming: true });
|
|
81
|
-
}
|
|
82
|
-
return out;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
function projectOne(item: TranscriptItem, options: ProjectOptions, nextSegment: () => number): ViewItem | undefined {
|
|
86
|
-
const base = { id: item.id, rev: item.revision };
|
|
87
|
-
switch (item.kind) {
|
|
88
|
-
case "user":
|
|
89
|
-
return { ...base, kind: "user", text: item.text, ...(item.images?.length ? { images: item.images.map(imageView) } : {}), ...(item.interrupted ? { interrupted: true } : {}) };
|
|
90
|
-
case "assistant": return item.text.trim() ? { ...base, kind: "assistant", text: item.text } : undefined;
|
|
91
|
-
case "thinking": return item.text.trim() ? { ...base, kind: "thinking", text: item.text, ...(options.hiddenThinkingLabel !== undefined ? { label: options.hiddenThinkingLabel } : {}) } : undefined;
|
|
92
|
-
case "notice": return { ...base, kind: "notice", text: item.text };
|
|
93
|
-
case "custom": {
|
|
94
|
-
const render = options.customMessage?.(item);
|
|
95
|
-
return render ? { ...base, kind: "lines", render } : { ...base, kind: "notice", text: `[${item.customType}] ${item.text}` };
|
|
96
|
-
}
|
|
97
|
-
case "tool": {
|
|
98
|
-
const custom = options.customTool?.(item);
|
|
99
|
-
const card = toolCard(item);
|
|
100
|
-
return { ...base, kind: "tool", name: item.name, summary: card.summary, status: card.status, ...(card.evidence ? { evidence: card.evidence } : {}),
|
|
101
|
-
...(custom ? {} : { preview: toolPreview(item) }), ...(item.result?.images?.length ? { images: item.result.images.map(imageView) } : {}),
|
|
102
|
-
detail: custom ?? (width => toolDetail(item, width, options.style?.())) };
|
|
103
|
-
}
|
|
104
|
-
case "shell": {
|
|
105
|
-
const code = item.exit; // the logged exit code is the evidence (D11)
|
|
106
|
-
// The log appends `exit N · status · reason` as the content's last line; the card's evidence already says it.
|
|
107
|
-
const output = item.output.replace(/^exit -?\d+[^\n]*\n?/, "").replace(/\n?exit -?\d+ · [^\n]*$/, "").replace(/\s+$/, "");
|
|
108
|
-
return { ...base, kind: "tool", name: item.excluded ? "!!" : "!", summary: item.command, status: code === 0 ? "ok" : "error", evidence: `exit ${code}`,
|
|
109
|
-
// Shell output keeps its last two lines in view while folded (refactor-tui-chrome-cleanup DoD 2).
|
|
110
|
-
preview: output.split("\n").filter(line => line.trim()).slice(-2),
|
|
111
|
-
// The exit code is already the card's evidence; the body is the output itself.
|
|
112
|
-
detail: () => (output || "(no output)").split("\n").slice(0, 200) };
|
|
113
|
-
}
|
|
114
|
-
case "segment": {
|
|
115
|
-
const node = item.nextNode ?? nextSegment();
|
|
116
|
-
return { ...base, kind: "segment", label: `node ${node} · ${SEGMENT_REASONS[item.reason] ?? item.reason}`,
|
|
117
|
-
detail: `handoff ${shortRef(item.handoff)}${item.nextModel ? ` → ${item.nextModel}` : ""} · history before this line is carried as structured state, not replayed` };
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
function shortRef(ref: string): string { return ref.split("/").pop() ?? ref; }
|
|
123
|
-
|
|
124
|
-
/** What the view needs of a logged image: its address, name and pixel size. Never the bytes (D8). */
|
|
125
|
-
export function imageView(image: ImageRef): ImageView {
|
|
126
|
-
return { sha256: image.sha256, name: image.name, width: image.width, height: image.height, ...(image.original ? { full: image.original.sha256 } : {}) };
|
|
127
|
-
}
|