@visual-z/xeno 0.8.3 → 0.9.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 +24 -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 -2066
- package/src/apps/startup-input.ts +0 -135
- package/src/apps/startup.ts +0 -1677
- package/src/apps/ui/app.ts +0 -1713
- 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 -3001
- 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/canvas.ts +0 -96
- package/src/tui/ui/colors.ts +0 -70
- package/src/tui/ui/composer.ts +0 -200
- 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/screen.ts +0 -548
- package/src/tui/ui/status.ts +0 -403
- package/src/tui/ui/styled.ts +0 -85
- package/src/tui/ui/tool-card.ts +0 -150
- package/src/tui/width.ts +0 -567
- package/src/vendor/pi-rich/LICENSE.upstream +0 -21
- package/src/vendor/pi-rich/UPSTREAM.json +0 -17
- package/src/vendor/pi-rich/clipboard-command.ts +0 -44
- package/src/vendor/pi-rich/exif-orientation.ts +0 -182
- package/src/vendor/pi-rich/html.ts +0 -51
- package/src/vendor/pi-rich/image-convert.ts +0 -49
- package/src/vendor/pi-rich/image-resize-core.ts +0 -164
- package/src/vendor/pi-rich/image-resize-worker.ts +0 -42
- package/src/vendor/pi-rich/image-resize.ts +0 -123
- package/src/vendor/pi-rich/mime.ts +0 -116
- package/src/vendor/pi-rich/photon.ts +0 -139
- package/src/vendor/pi-rich/syntax-highlight.ts +0 -214
package/src/store/worktree.ts
DELETED
|
@@ -1,278 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Git worktree isolation for branches that write.
|
|
3
|
-
*
|
|
4
|
-
* A worktree is a second checkout sharing one object database, which is why
|
|
5
|
-
* it is the isolation primitive parallel agents converged on: each branch
|
|
6
|
-
* edits its own files, and conflicts surface at merge time through ordinary
|
|
7
|
-
* git tooling instead of as silent overwrites during the work.
|
|
8
|
-
*
|
|
9
|
-
* What this does NOT isolate, stated because assuming otherwise is how these
|
|
10
|
-
* setups break:
|
|
11
|
-
*
|
|
12
|
-
* ENVIRONMENT. Ports, databases, and anything else outside the working
|
|
13
|
-
* directory are still shared. Two branches running a dev server or a
|
|
14
|
-
* migration will collide, and a test can fail because a sibling took the
|
|
15
|
-
* port rather than because the branch is wrong. That needs containers.
|
|
16
|
-
*
|
|
17
|
-
* ESCAPE. A branch can point git back at the shared checkout with `git -C`
|
|
18
|
-
* or GIT_DIR, which has been an actual reported escape. Preventing it is a
|
|
19
|
-
* sandbox's job, and this project has no sandbox layer; the isolation here
|
|
20
|
-
* is for cooperative branches, not adversarial ones.
|
|
21
|
-
*/
|
|
22
|
-
|
|
23
|
-
import { mkdir, mkdtemp, rm, realpath } from "node:fs/promises";
|
|
24
|
-
import { existsSync } from "node:fs";
|
|
25
|
-
import { createHash } from "node:crypto";
|
|
26
|
-
import { basename, join } from "node:path";
|
|
27
|
-
import { tmpdir } from "node:os";
|
|
28
|
-
|
|
29
|
-
/** Branch prefix, so a stale worktree is identifiable months later. */
|
|
30
|
-
export const WORKTREE_BRANCH_PREFIX = "xeno/";
|
|
31
|
-
|
|
32
|
-
export interface WorktreeInfo {
|
|
33
|
-
path: string;
|
|
34
|
-
branch: string | null;
|
|
35
|
-
locked: boolean;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export type CreateResult =
|
|
39
|
-
| { ok: true; path: string; branch: string; snapshot: { head: string; tree: string } }
|
|
40
|
-
| { ok: false; error: string };
|
|
41
|
-
|
|
42
|
-
export interface CreateOptions {
|
|
43
|
-
/**
|
|
44
|
-
* Where the checkout goes. Default: a fresh temp directory, which suits a
|
|
45
|
-
* writing branch that lives for one subtask. A model-entered worktree is
|
|
46
|
-
* kept across sessions, so it names a stable parent and its own leaf.
|
|
47
|
-
*/
|
|
48
|
-
location?: { parent: string; leaf: string };
|
|
49
|
-
/** Carry the parent's uncommitted files (default, as a branch needs). */
|
|
50
|
-
includeChanges?: boolean;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* The stable home of model-entered worktrees for one repository.
|
|
55
|
-
*
|
|
56
|
-
* Under xeno's config directory rather than the project: inside `.xeno` it
|
|
57
|
-
* would collide with the rule that session state is never edited, and a
|
|
58
|
-
* sibling directory leaves xeno's checkouts in the user's project tree (Codex
|
|
59
|
-
* keeps its worktrees under its own home for the same reason). The digest of
|
|
60
|
-
* the repository path keeps two clones with the same name apart.
|
|
61
|
-
*/
|
|
62
|
-
export function persistentWorktreeParent(configDir: string, repo: string): string {
|
|
63
|
-
const digest = createHash("sha256").update(repo).digest("hex").slice(0, 8);
|
|
64
|
-
return join(configDir, "worktrees", `${basename(repo)}-${digest}`);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/** The repository's main checkout: `git worktree list` always lists it first. */
|
|
68
|
-
export async function mainWorktree(dir: string): Promise<string | null> {
|
|
69
|
-
const listed = await listWorktrees(dir).catch(() => []);
|
|
70
|
-
return listed[0]?.path ?? null;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
export type RemoveResult =
|
|
74
|
-
| { ok: true }
|
|
75
|
-
| { ok: false; error: string; dirty: boolean };
|
|
76
|
-
|
|
77
|
-
interface GitRun {
|
|
78
|
-
code: number;
|
|
79
|
-
stdout: string;
|
|
80
|
-
stderr: string;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
function git(cwd: string, args: string[], env?: Record<string, string | undefined>): GitRun {
|
|
84
|
-
const p = Bun.spawnSync(["git", ...args], { cwd, ...(env ? { env } : {}) });
|
|
85
|
-
const dec = new TextDecoder();
|
|
86
|
-
return {
|
|
87
|
-
code: p.exitCode,
|
|
88
|
-
stdout: dec.decode(p.stdout),
|
|
89
|
-
stderr: dec.decode(p.stderr),
|
|
90
|
-
};
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
export async function isGitRepo(dir: string): Promise<boolean> {
|
|
94
|
-
return git(dir, ["rev-parse", "--git-dir"]).code === 0;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
export async function createWorktree(repo: string, name: string, options: CreateOptions = {}): Promise<CreateResult> {
|
|
98
|
-
// Refused rather than degraded. Falling back to the shared directory is the
|
|
99
|
-
// exact failure isolation exists to prevent: parallel writing branches
|
|
100
|
-
// would overwrite each other with nothing to indicate isolation was never
|
|
101
|
-
// in place.
|
|
102
|
-
if (!(await isGitRepo(repo))) {
|
|
103
|
-
return {
|
|
104
|
-
ok: false,
|
|
105
|
-
error:
|
|
106
|
-
`${repo} is not a git repository, so a writing branch cannot be isolated. ` +
|
|
107
|
-
"Initialise one, or run the branch read-only.",
|
|
108
|
-
};
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
// O.4: snapshot the working copy through a private index. Never stash, reset,
|
|
112
|
-
// or stage the user's index merely to construct a child input.
|
|
113
|
-
const root = git(repo, ["rev-parse", "--show-toplevel"]);
|
|
114
|
-
if (root.code !== 0 || await realpath(repo) !== await realpath(root.stdout.trim())) {
|
|
115
|
-
return { ok: false, error: "writing branch input must be the repository root" };
|
|
116
|
-
}
|
|
117
|
-
const head = git(repo, ["rev-parse", "--verify", "HEAD"]);
|
|
118
|
-
if (head.code !== 0) return { ok: false, error: "writing branch input requires an existing HEAD commit" };
|
|
119
|
-
const unmerged = git(repo, ["ls-files", "--unmerged"]);
|
|
120
|
-
if (unmerged.code !== 0 || unmerged.stdout.length > 0) return { ok: false, error: "resolve index conflicts before creating a writing branch" };
|
|
121
|
-
const staged = git(repo, ["ls-files", "--stage", "-z"]);
|
|
122
|
-
if (staged.code !== 0 || staged.stdout.split("\0").some(entry => entry.startsWith("160000 "))) {
|
|
123
|
-
return { ok: false, error: "writing branch snapshots do not yet support submodules" };
|
|
124
|
-
}
|
|
125
|
-
const scratch = await mkdtemp(join(tmpdir(), "xeno-input-index-"));
|
|
126
|
-
let tree: string;
|
|
127
|
-
try {
|
|
128
|
-
const env = { ...process.env, GIT_INDEX_FILE: join(scratch, "index") };
|
|
129
|
-
for (const args of [["read-tree", head.stdout.trim()], ["add", "--all", "--", "."]]) {
|
|
130
|
-
const result = git(repo, args, env);
|
|
131
|
-
if (result.code !== 0) return { ok: false, error: `cannot capture branch input: ${result.stderr.trim()}` };
|
|
132
|
-
}
|
|
133
|
-
const captured = git(repo, ["ls-files", "--stage", "-z"], env);
|
|
134
|
-
if (captured.code !== 0 || captured.stdout.split("\0").some(entry => entry.startsWith("160000 "))) {
|
|
135
|
-
return { ok: false, error: "writing branch snapshots do not yet support nested repositories or submodules" };
|
|
136
|
-
}
|
|
137
|
-
const written = git(repo, ["write-tree"], env);
|
|
138
|
-
if (written.code !== 0) return { ok: false, error: `cannot capture branch input tree: ${written.stderr.trim()}` };
|
|
139
|
-
tree = written.stdout.trim();
|
|
140
|
-
} finally { await rm(scratch, { recursive: true, force: true }); }
|
|
141
|
-
|
|
142
|
-
const branch = `${WORKTREE_BRANCH_PREFIX}${name}`;
|
|
143
|
-
if (git(repo, ["rev-parse", "--verify", "--quiet", `refs/heads/${branch}`]).code === 0) {
|
|
144
|
-
return { ok: false, error: `branch ${branch} already exists; choose another name or work in its existing checkout` };
|
|
145
|
-
}
|
|
146
|
-
let path: string;
|
|
147
|
-
if (options.location) {
|
|
148
|
-
await mkdir(options.location.parent, { recursive: true });
|
|
149
|
-
// Resolved for the same reason as the temp path below.
|
|
150
|
-
path = join(await realpath(options.location.parent), options.location.leaf);
|
|
151
|
-
if (existsSync(path)) return { ok: false, error: `${path} already exists; choose another name` };
|
|
152
|
-
} else {
|
|
153
|
-
const created = await mkdtemp(join(tmpdir(), `xeno-wt-${name}-`));
|
|
154
|
-
// Resolved through symlinks, because git reports the real path and a
|
|
155
|
-
// caller comparing the two would never match. On macOS /var is a symlink
|
|
156
|
-
// to /private/var, so every temp path differs by default.
|
|
157
|
-
path = await realpath(created);
|
|
158
|
-
// mkdtemp created it; git worktree add needs the path not to exist.
|
|
159
|
-
await rm(path, { recursive: true, force: true });
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
// --lock in the same call: locking afterwards leaves a window in which a
|
|
163
|
-
// concurrent prune can remove the worktree that was just created.
|
|
164
|
-
const add = git(repo, ["worktree", "add", "--lock", "-b", branch, path, head.stdout.trim()]);
|
|
165
|
-
if (add.code !== 0) {
|
|
166
|
-
return { ok: false, error: `git worktree add failed: ${add.stderr.trim()}` };
|
|
167
|
-
}
|
|
168
|
-
if (options.includeChanges === false) return { ok: true, path, branch, snapshot: { head: head.stdout.trim(), tree: git(path, ["rev-parse", "HEAD^{tree}"]).stdout.trim() } };
|
|
169
|
-
// Only this freshly created disposable checkout is reset. The child index
|
|
170
|
-
// carries the parent's uncommitted input, so normal diff/inspection still sees it.
|
|
171
|
-
const hydrate = git(path, ["read-tree", "--reset", "-u", tree]);
|
|
172
|
-
if (hydrate.code !== 0) {
|
|
173
|
-
const removed = await removeWorktree(repo, path, branch, { force: true });
|
|
174
|
-
return { ok: false, error: `cannot materialize branch input: ${hydrate.stderr.trim()}` +
|
|
175
|
-
(removed.ok ? "" : `; cleanup required at ${path}: ${removed.error}`) };
|
|
176
|
-
}
|
|
177
|
-
return { ok: true, path, branch, snapshot: { head: head.stdout.trim(), tree } };
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
/**
|
|
181
|
-
* Parses `--porcelain -z`, the only output format documented as stable
|
|
182
|
-
* across git versions and user configuration. `-z` additionally survives a
|
|
183
|
-
* path containing a newline, which a line-based parse would split into two
|
|
184
|
-
* bogus entries.
|
|
185
|
-
*/
|
|
186
|
-
export async function listWorktrees(repo: string): Promise<WorktreeInfo[]> {
|
|
187
|
-
const out = git(repo, ["worktree", "list", "--porcelain", "-z"]);
|
|
188
|
-
if (out.code !== 0) return [];
|
|
189
|
-
|
|
190
|
-
const worktrees: WorktreeInfo[] = [];
|
|
191
|
-
let current: Partial<WorktreeInfo> = {};
|
|
192
|
-
for (const record of out.stdout.split("\0")) {
|
|
193
|
-
if (record === "") {
|
|
194
|
-
// A blank record terminates one worktree's attributes.
|
|
195
|
-
if (current.path) {
|
|
196
|
-
worktrees.push({
|
|
197
|
-
path: current.path,
|
|
198
|
-
branch: current.branch ?? null,
|
|
199
|
-
locked: current.locked ?? false,
|
|
200
|
-
});
|
|
201
|
-
}
|
|
202
|
-
current = {};
|
|
203
|
-
continue;
|
|
204
|
-
}
|
|
205
|
-
const [key, ...rest] = record.split(" ");
|
|
206
|
-
const value = rest.join(" ");
|
|
207
|
-
if (key === "worktree") current.path = value;
|
|
208
|
-
else if (key === "branch") current.branch = value.replace(/^refs\/heads\//, "");
|
|
209
|
-
else if (key === "locked") current.locked = true;
|
|
210
|
-
}
|
|
211
|
-
if (current.path) {
|
|
212
|
-
worktrees.push({
|
|
213
|
-
path: current.path,
|
|
214
|
-
branch: current.branch ?? null,
|
|
215
|
-
locked: current.locked ?? false,
|
|
216
|
-
});
|
|
217
|
-
}
|
|
218
|
-
return worktrees;
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
/**
|
|
222
|
-
* Removes a worktree, its registration and its branch.
|
|
223
|
-
*
|
|
224
|
-
* A dirty worktree is reported rather than forced. The refusal is evidence
|
|
225
|
-
* to look at: uncommitted changes there may be exactly the work the branch
|
|
226
|
-
* was for, and discarding them silently is the destructive-restore mistake
|
|
227
|
-
* in a different costume. `force` exists for a caller that has decided.
|
|
228
|
-
*/
|
|
229
|
-
export async function removeWorktree(
|
|
230
|
-
repo: string,
|
|
231
|
-
path: string,
|
|
232
|
-
branch: string | null,
|
|
233
|
-
opts: { force?: boolean } = {},
|
|
234
|
-
): Promise<RemoveResult> {
|
|
235
|
-
// Created with --lock, so unlock first; a locked worktree otherwise needs
|
|
236
|
-
// --force twice, which would conflate "locked" with "dirty".
|
|
237
|
-
git(repo, ["worktree", "unlock", path]);
|
|
238
|
-
|
|
239
|
-
const args = ["worktree", "remove", path];
|
|
240
|
-
if (opts.force) args.push("--force");
|
|
241
|
-
const removed = git(repo, args);
|
|
242
|
-
|
|
243
|
-
if (removed.code !== 0) {
|
|
244
|
-
const dirty = /contains modified or untracked files|is dirty/i.test(removed.stderr);
|
|
245
|
-
return {
|
|
246
|
-
ok: false,
|
|
247
|
-
error: dirty
|
|
248
|
-
? `${path} has uncommitted changes. Inspect them before removing, ` +
|
|
249
|
-
"or pass force to discard them."
|
|
250
|
-
: `git worktree remove failed: ${removed.stderr.trim()}`,
|
|
251
|
-
dirty,
|
|
252
|
-
};
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
// Removing a worktree never deletes its branch, so a run of branches would
|
|
256
|
-
// otherwise leave a branch behind for every one of them.
|
|
257
|
-
if (branch) git(repo, ["branch", "-D", branch]);
|
|
258
|
-
return { ok: true };
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
/**
|
|
262
|
-
* Clears registrations left behind by hand-deleted directories.
|
|
263
|
-
*
|
|
264
|
-
* Only for that case. A worktree that was MOVED must be repaired instead --
|
|
265
|
-
* pruning it discards a working directory that still exists.
|
|
266
|
-
*/
|
|
267
|
-
export async function pruneWorktrees(repo: string): Promise<void> {
|
|
268
|
-
// A lock exists to stop pruning, and createWorktree takes one -- so a
|
|
269
|
-
// hand-deleted worktree of ours would stay registered forever without
|
|
270
|
-
// unlocking first. Only ours: the prefix check keeps this from unlocking
|
|
271
|
-
// a worktree someone locked deliberately.
|
|
272
|
-
for (const wt of await listWorktrees(repo)) {
|
|
273
|
-
if (wt.locked && wt.branch?.startsWith(WORKTREE_BRANCH_PREFIX)) {
|
|
274
|
-
git(repo, ["worktree", "unlock", wt.path]);
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
git(repo, ["worktree", "prune"]);
|
|
278
|
-
}
|
package/src/tui/ansi-spans.ts
DELETED
|
@@ -1,126 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ANSI → styled spans. Pure; no renderer import.
|
|
3
|
-
*
|
|
4
|
-
* Message content (Markdown with highlighting, Mermaid, diffs, tool views)
|
|
5
|
-
* and every extension renderer produce `string[]` lines with SGR escapes —
|
|
6
|
-
* that is the Pi-compatible component contract extensions already rely on.
|
|
7
|
-
* The OpenTUI view draws cells from styled chunks, not escapes, and ships no
|
|
8
|
-
* ANSI parser, so this module is the one bridge: it turns those lines into
|
|
9
|
-
* spans the view converts to TextChunks. Keeping the bridge here (instead of
|
|
10
|
-
* re-implementing every renderer natively) keeps the extension API and the
|
|
11
|
-
* tested renderers intact across the UI rewrite (D20).
|
|
12
|
-
*
|
|
13
|
-
* Supported: SGR 0/1/2/3/4/7/9/22/23/24/27/29, 30–37/90–97/39, 40–47/100–107/49,
|
|
14
|
-
* 38;5;n / 48;5;n, 38;2;r;g;b / 48;2;r;g;b, OSC 8 hyperlinks. Every other
|
|
15
|
-
* CSI/OSC/APC (cursor moves, Pi's cursor marker, kitty graphics) is dropped:
|
|
16
|
-
* a renderable lays out its own cells, so movement escapes have no meaning.
|
|
17
|
-
*/
|
|
18
|
-
|
|
19
|
-
export type SpanColor = { kind: "index"; index: number } | { kind: "rgb"; r: number; g: number; b: number };
|
|
20
|
-
|
|
21
|
-
export interface SpanStyle {
|
|
22
|
-
readonly fg?: SpanColor;
|
|
23
|
-
readonly bg?: SpanColor;
|
|
24
|
-
readonly bold?: boolean;
|
|
25
|
-
readonly dim?: boolean;
|
|
26
|
-
readonly italic?: boolean;
|
|
27
|
-
readonly underline?: boolean;
|
|
28
|
-
readonly reverse?: boolean;
|
|
29
|
-
readonly strikethrough?: boolean;
|
|
30
|
-
readonly link?: string;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export interface Span { readonly text: string; readonly style: SpanStyle }
|
|
34
|
-
|
|
35
|
-
type Mutable = { -readonly [K in keyof SpanStyle]: SpanStyle[K] };
|
|
36
|
-
|
|
37
|
-
function applySgr(style: Mutable, params: number[]): void {
|
|
38
|
-
if (params.length === 0) params = [0];
|
|
39
|
-
for (let i = 0; i < params.length; i++) {
|
|
40
|
-
const p = params[i]!;
|
|
41
|
-
if (p === 0) { for (const key of Object.keys(style) as (keyof Mutable)[]) if (key !== "link") delete style[key]; }
|
|
42
|
-
else if (p === 1) style.bold = true;
|
|
43
|
-
else if (p === 2) style.dim = true;
|
|
44
|
-
else if (p === 3) style.italic = true;
|
|
45
|
-
else if (p === 4) style.underline = true;
|
|
46
|
-
else if (p === 7) style.reverse = true;
|
|
47
|
-
else if (p === 9) style.strikethrough = true;
|
|
48
|
-
else if (p === 22) { delete style.bold; delete style.dim; }
|
|
49
|
-
else if (p === 23) delete style.italic;
|
|
50
|
-
else if (p === 24) delete style.underline;
|
|
51
|
-
else if (p === 27) delete style.reverse;
|
|
52
|
-
else if (p === 29) delete style.strikethrough;
|
|
53
|
-
else if (p >= 30 && p <= 37) style.fg = { kind: "index", index: p - 30 };
|
|
54
|
-
else if (p >= 90 && p <= 97) style.fg = { kind: "index", index: p - 90 + 8 };
|
|
55
|
-
else if (p === 39) delete style.fg;
|
|
56
|
-
else if (p >= 40 && p <= 47) style.bg = { kind: "index", index: p - 40 };
|
|
57
|
-
else if (p >= 100 && p <= 107) style.bg = { kind: "index", index: p - 100 + 8 };
|
|
58
|
-
else if (p === 49) delete style.bg;
|
|
59
|
-
else if (p === 38 || p === 48) {
|
|
60
|
-
const mode = params[i + 1];
|
|
61
|
-
let color: SpanColor | undefined;
|
|
62
|
-
if (mode === 5 && params[i + 2] !== undefined) { color = { kind: "index", index: clamp(params[i + 2]!) }; i += 2; }
|
|
63
|
-
else if (mode === 2 && params[i + 4] !== undefined) { color = { kind: "rgb", r: clamp(params[i + 2]!), g: clamp(params[i + 3]!), b: clamp(params[i + 4]!) }; i += 4; }
|
|
64
|
-
else break; // malformed extended colour: ignore the rest of this sequence
|
|
65
|
-
if (p === 38) style.fg = color; else style.bg = color;
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
function clamp(n: number): number { return Math.max(0, Math.min(255, Number.isFinite(n) ? n : 0)); }
|
|
71
|
-
|
|
72
|
-
function freeze(style: Mutable): SpanStyle { return { ...style }; }
|
|
73
|
-
|
|
74
|
-
function sameStyle(a: SpanStyle, b: SpanStyle): boolean { return JSON.stringify(a) === JSON.stringify(b); }
|
|
75
|
-
|
|
76
|
-
/** Parse one line. Adjacent text with equal style merges into one span. */
|
|
77
|
-
export function ansiSpans(line: string): Span[] {
|
|
78
|
-
const spans: Span[] = [];
|
|
79
|
-
const style: Mutable = {};
|
|
80
|
-
let text = "";
|
|
81
|
-
const flush = () => {
|
|
82
|
-
if (!text) return;
|
|
83
|
-
const frozen = freeze(style);
|
|
84
|
-
const last = spans[spans.length - 1];
|
|
85
|
-
if (last && sameStyle(last.style, frozen)) spans[spans.length - 1] = { text: last.text + text, style: frozen };
|
|
86
|
-
else spans.push({ text, style: frozen });
|
|
87
|
-
text = "";
|
|
88
|
-
};
|
|
89
|
-
let i = 0;
|
|
90
|
-
while (i < line.length) {
|
|
91
|
-
const ch = line[i]!;
|
|
92
|
-
if (ch !== "\x1b") { text += ch; i++; continue; }
|
|
93
|
-
const kind = line[i + 1];
|
|
94
|
-
if (kind === "[") {
|
|
95
|
-
let j = i + 2;
|
|
96
|
-
while (j < line.length && !/[@-~]/.test(line[j]!)) j++;
|
|
97
|
-
const body = line.slice(i + 2, j);
|
|
98
|
-
if (line[j] === "m" && /^[0-9;]*$/.test(body)) {
|
|
99
|
-
flush();
|
|
100
|
-
applySgr(style, body === "" ? [] : body.split(";").map(part => (part === "" ? 0 : Number(part))));
|
|
101
|
-
}
|
|
102
|
-
i = j + 1;
|
|
103
|
-
continue;
|
|
104
|
-
}
|
|
105
|
-
if (kind === "]" || kind === "_" || kind === "P") {
|
|
106
|
-
let j = i + 2;
|
|
107
|
-
while (j < line.length && line[j] !== "\x07" && !(line[j] === "\x1b" && line[j + 1] === "\\")) j++;
|
|
108
|
-
const body = line.slice(i + 2, j);
|
|
109
|
-
if (kind === "]" && body.startsWith("8;")) {
|
|
110
|
-
flush();
|
|
111
|
-
const url = body.slice(body.indexOf(";", 2) + 1);
|
|
112
|
-
if (url) style.link = url; else delete style.link;
|
|
113
|
-
}
|
|
114
|
-
i = line[j] === "\x07" ? j + 1 : j + 2;
|
|
115
|
-
continue;
|
|
116
|
-
}
|
|
117
|
-
i += 2;
|
|
118
|
-
}
|
|
119
|
-
flush();
|
|
120
|
-
return spans;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
/** Visible text of a line: what a terminal would print, escapes removed. */
|
|
124
|
-
export function plainText(line: string): string {
|
|
125
|
-
return ansiSpans(line).map(span => span.text).join("");
|
|
126
|
-
}
|
package/src/tui/diff.ts
DELETED
|
@@ -1,160 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Line diff for an edit's old/new snippets.
|
|
3
|
-
*
|
|
4
|
-
* Ported from pi (earendil-works/pi, MIT) — packages/coding-agent/src/core/tools/edit-diff.ts
|
|
5
|
-
* `generateDiffString`: context around changed lines, skip the rest. We cannot
|
|
6
|
-
* take the `diff` package (tui may not import npm), so the hunks are an LCS
|
|
7
|
-
* of lines rather than Myers. Edits carry snippets, not whole files, so O(n·m)
|
|
8
|
-
* stays small.
|
|
9
|
-
*
|
|
10
|
-
* Why not the whole file: the information in an edit is what changed. Dumping
|
|
11
|
-
* the destination file fills the live region and makes the user hunt.
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
import { PLAIN_ATTRS, paint, type Attrs, type Style } from "./style.ts";
|
|
15
|
-
import { displayWidth } from "./width.ts";
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Unchanged lines kept on each side of a hunk. From pi's generateDiffString
|
|
19
|
-
* default (4). Assumed, not measured: more context drowns the `+`/`-` lines
|
|
20
|
-
* this view exists to show.
|
|
21
|
-
*/
|
|
22
|
-
const CONTEXT_LINES = 4;
|
|
23
|
-
|
|
24
|
-
export type DiffOp = { kind: "eq" | "del" | "add"; line: string };
|
|
25
|
-
|
|
26
|
-
export function diffLines(oldText: string, newText: string): DiffOp[] {
|
|
27
|
-
const a = oldText.split("\n");
|
|
28
|
-
const b = newText.split("\n");
|
|
29
|
-
const n = a.length;
|
|
30
|
-
const m = b.length;
|
|
31
|
-
const dp: number[][] = Array.from({ length: n + 1 }, () => Array<number>(m + 1).fill(0));
|
|
32
|
-
for (let i = n - 1; i >= 0; i--) {
|
|
33
|
-
for (let j = m - 1; j >= 0; j--) {
|
|
34
|
-
dp[i]![j] = a[i] === b[j] ? dp[i + 1]![j + 1]! + 1 : Math.max(dp[i + 1]![j]!, dp[i]![j + 1]!);
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
const out: DiffOp[] = [];
|
|
38
|
-
let i = 0;
|
|
39
|
-
let j = 0;
|
|
40
|
-
while (i < n && j < m) {
|
|
41
|
-
if (a[i] === b[j]) {
|
|
42
|
-
out.push({ kind: "eq", line: a[i]! });
|
|
43
|
-
i++;
|
|
44
|
-
j++;
|
|
45
|
-
} else if (dp[i + 1]![j]! >= dp[i]![j + 1]!) {
|
|
46
|
-
out.push({ kind: "del", line: a[i]! });
|
|
47
|
-
i++;
|
|
48
|
-
} else {
|
|
49
|
-
out.push({ kind: "add", line: b[j]! });
|
|
50
|
-
j++;
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
while (i < n) {
|
|
54
|
-
out.push({ kind: "del", line: a[i]! });
|
|
55
|
-
i++;
|
|
56
|
-
}
|
|
57
|
-
while (j < m) {
|
|
58
|
-
out.push({ kind: "add", line: b[j]! });
|
|
59
|
-
j++;
|
|
60
|
-
}
|
|
61
|
-
return out;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* Keep only ops within CONTEXT_LINES of a change. Skipped equal runs become
|
|
66
|
-
* a dim `... (N lines)` so the omission is counted, not silent.
|
|
67
|
-
*/
|
|
68
|
-
export function withContext(ops: readonly DiffOp[]): DiffOp[] {
|
|
69
|
-
const keep = new Array<boolean>(ops.length).fill(false);
|
|
70
|
-
for (let i = 0; i < ops.length; i++) {
|
|
71
|
-
if (ops[i]!.kind === "eq") continue;
|
|
72
|
-
const from = Math.max(0, i - CONTEXT_LINES);
|
|
73
|
-
const to = Math.min(ops.length - 1, i + CONTEXT_LINES);
|
|
74
|
-
for (let k = from; k <= to; k++) keep[k] = true;
|
|
75
|
-
}
|
|
76
|
-
const out: DiffOp[] = [];
|
|
77
|
-
let i = 0;
|
|
78
|
-
while (i < ops.length) {
|
|
79
|
-
if (keep[i]) {
|
|
80
|
-
out.push(ops[i]!);
|
|
81
|
-
i++;
|
|
82
|
-
continue;
|
|
83
|
-
}
|
|
84
|
-
let j = i;
|
|
85
|
-
while (j < ops.length && !keep[j]) j++;
|
|
86
|
-
const skipped = j - i;
|
|
87
|
-
if (skipped > 0) out.push({ kind: "eq", line: `... (${skipped} lines)` });
|
|
88
|
-
i = j;
|
|
89
|
-
}
|
|
90
|
-
return out;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
const DEL: Attrs = { ...PLAIN_ATTRS, red: true };
|
|
94
|
-
const ADD: Attrs = { ...PLAIN_ATTRS, green: true };
|
|
95
|
-
const EQ: Attrs = { ...PLAIN_ATTRS, dim: true };
|
|
96
|
-
|
|
97
|
-
export function renderDiff(oldText: string, newText: string, width: number, style: Style): string[] {
|
|
98
|
-
const ops = withContext(diffLines(oldText, newText));
|
|
99
|
-
const out: string[] = [];
|
|
100
|
-
for (const op of ops) {
|
|
101
|
-
const mark = op.kind === "del" ? "-" : op.kind === "add" ? "+" : " ";
|
|
102
|
-
const attrs = op.kind === "del" ? DEL : op.kind === "add" ? ADD : EQ;
|
|
103
|
-
out.push(...emitPrefixed(mark, op.line, width, style, attrs));
|
|
104
|
-
}
|
|
105
|
-
return out;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/**
|
|
109
|
-
* Columns left for the body after the prefix. A prefix that does not
|
|
110
|
-
* leave at least one column is dropped: wrapPlain floors a zero budget
|
|
111
|
-
* at 1, so keeping the prefix would paint prefixW+1 columns and the
|
|
112
|
-
* terminal would wrap. The line-diff renderer then counts one row where
|
|
113
|
-
* the screen used two — the same trap as a 0-column pty (`usableColumns`
|
|
114
|
-
* in terminal.ts).
|
|
115
|
-
*/
|
|
116
|
-
export function innerWidth(prefix: string, width: number): number {
|
|
117
|
-
const w = Math.max(0, Math.floor(width));
|
|
118
|
-
if (w <= 0) return 0;
|
|
119
|
-
const prefixW = displayWidth(prefix);
|
|
120
|
-
return prefixW > 0 && prefixW < w ? w - prefixW : w;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
export function emitPrefixed(
|
|
124
|
-
prefix: string,
|
|
125
|
-
body: string,
|
|
126
|
-
width: number,
|
|
127
|
-
style: Style,
|
|
128
|
-
attrs: Attrs,
|
|
129
|
-
): string[] {
|
|
130
|
-
const w = Math.max(0, Math.floor(width));
|
|
131
|
-
if (w <= 0) return [""];
|
|
132
|
-
const prefixW = displayWidth(prefix);
|
|
133
|
-
const inner = innerWidth(prefix, w);
|
|
134
|
-
const rows = wrapPlain(body, inner);
|
|
135
|
-
if (inner === w) {
|
|
136
|
-
return rows.map((row) => paint(row, attrs, style));
|
|
137
|
-
}
|
|
138
|
-
const pad = " ".repeat(prefixW);
|
|
139
|
-
return rows.map((row, i) => paint((i === 0 ? prefix : pad) + row, attrs, style));
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
export function wrapPlain(text: string, width: number): string[] {
|
|
143
|
-
if (width <= 0) return [""];
|
|
144
|
-
const lines: string[] = [];
|
|
145
|
-
let current = "";
|
|
146
|
-
let currentW = 0;
|
|
147
|
-
for (const ch of text) {
|
|
148
|
-
const cw = displayWidth(ch);
|
|
149
|
-
if (cw > 0 && currentW + cw > width && current.length > 0) {
|
|
150
|
-
lines.push(current);
|
|
151
|
-
current = ch;
|
|
152
|
-
currentW = cw;
|
|
153
|
-
} else {
|
|
154
|
-
current += ch;
|
|
155
|
-
currentW += cw;
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
lines.push(current);
|
|
159
|
-
return lines;
|
|
160
|
-
}
|
package/src/tui/exhaustive.ts
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Exhaustiveness for discriminated unions.
|
|
3
|
-
*
|
|
4
|
-
* Copied rather than imported from core: tui may not import src/ outside
|
|
5
|
-
* itself, and the boundary checker exists to keep that true. The two copies
|
|
6
|
-
* must stay one function; if they drift, the newer union is the one that
|
|
7
|
-
* will fail to compile first.
|
|
8
|
-
*/
|
|
9
|
-
export function assertNever(value: never, context: string): never {
|
|
10
|
-
throw new Error(
|
|
11
|
-
`${context}: unhandled variant ${JSON.stringify(value)} -- ` +
|
|
12
|
-
"a union gained a member without every switch over it being updated",
|
|
13
|
-
);
|
|
14
|
-
}
|
package/src/tui/fuzzy.ts
DELETED
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Subsequence fuzzy matching with a stable rank.
|
|
3
|
-
*
|
|
4
|
-
* Consecutive hits and word-start hits score better (pi-tui's scheme,
|
|
5
|
-
* lower is better). Ties keep the original index: an unstable sort would
|
|
6
|
-
* reshuffle the select list between two renders, and Enter would confirm
|
|
7
|
-
* a different item than the one under the cursor.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
export interface FuzzyMatch {
|
|
11
|
-
readonly text: string;
|
|
12
|
-
readonly score: number;
|
|
13
|
-
readonly index: number;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Lower is better. `null` if `query` is not a subsequence of `text`.
|
|
18
|
-
* Case-insensitive. Empty query matches with score 0.
|
|
19
|
-
*/
|
|
20
|
-
export function fuzzyMatch(text: string, query: string): number | null {
|
|
21
|
-
if (query.length === 0) return 0;
|
|
22
|
-
return score(text, query);
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export function fuzzyFilter(candidates: readonly string[], query: string): string[] {
|
|
26
|
-
if (query.length === 0) return [...candidates];
|
|
27
|
-
const ranked: FuzzyMatch[] = [];
|
|
28
|
-
for (let index = 0; index < candidates.length; index++) {
|
|
29
|
-
const text = candidates[index]!;
|
|
30
|
-
const s = score(text, query);
|
|
31
|
-
if (s === null) continue;
|
|
32
|
-
ranked.push({ text, score: s, index });
|
|
33
|
-
}
|
|
34
|
-
ranked.sort((a, b) => a.score - b.score || a.index - b.index);
|
|
35
|
-
return ranked.map((r) => r.text);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Leftmost greedy subsequence, scored like pi-tui:
|
|
40
|
-
* consecutive run bonus, word-boundary bonus, later-match penalty.
|
|
41
|
-
* Walks code points so a CJK query cannot land on a UTF-16 half-unit.
|
|
42
|
-
*/
|
|
43
|
-
function score(text: string, query: string): number | null {
|
|
44
|
-
const t = [...text.toLowerCase()];
|
|
45
|
-
const q = [...query.toLowerCase()];
|
|
46
|
-
if (q.length > t.length) return null;
|
|
47
|
-
|
|
48
|
-
let qi = 0;
|
|
49
|
-
let total = 0;
|
|
50
|
-
let last = -1;
|
|
51
|
-
let consecutive = 0;
|
|
52
|
-
|
|
53
|
-
for (let i = 0; i < t.length && qi < q.length; i++) {
|
|
54
|
-
if (t[i] !== q[qi]) continue;
|
|
55
|
-
const prev = i === 0 ? "" : t[i - 1]!;
|
|
56
|
-
const boundary = i === 0 || /[\s\-_./:]/.test(prev);
|
|
57
|
-
|
|
58
|
-
if (last === i - 1) {
|
|
59
|
-
consecutive++;
|
|
60
|
-
total -= consecutive * 5;
|
|
61
|
-
} else {
|
|
62
|
-
consecutive = 0;
|
|
63
|
-
if (last >= 0) total += (i - last - 1) * 2;
|
|
64
|
-
}
|
|
65
|
-
if (boundary) total -= 10;
|
|
66
|
-
total += i * 0.1;
|
|
67
|
-
|
|
68
|
-
last = i;
|
|
69
|
-
qi++;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
if (qi < q.length) return null;
|
|
73
|
-
if (q.join("") === t.join("")) total -= 100;
|
|
74
|
-
return total;
|
|
75
|
-
}
|