tmux-ide 2.6.0 → 2.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -9
- package/bin/cli.js +1024 -519
- package/bin/cli.ts +63 -6
- package/bunfig.toml +4 -0
- package/package.json +18 -7
- package/packages/contracts/package.json +22 -0
- package/packages/contracts/src/__tests__/ide-config.test.ts +46 -0
- package/packages/contracts/src/__tests__/terminals.test.ts +87 -0
- package/packages/contracts/src/actions-contract.ts +310 -0
- package/packages/contracts/src/actions-errors.ts +41 -0
- package/packages/contracts/src/domain.ts +36 -0
- package/packages/contracts/src/ide-config.ts +170 -0
- package/packages/contracts/src/index.ts +24 -0
- package/packages/contracts/src/lib-internal/auth.ts +13 -0
- package/packages/contracts/src/lib-internal/hq.ts +38 -0
- package/packages/contracts/src/terminals.ts +116 -0
- package/packages/contracts/src/tmux.ts +60 -0
- package/packages/contracts/src/workspace.ts +67 -0
- package/packages/daemon/dist/agent-explain.d.ts +8 -1
- package/packages/daemon/dist/agent-explain.js +19 -3
- package/packages/daemon/dist/lib/tui-binary.d.ts +57 -0
- package/packages/daemon/dist/lib/tui-binary.js +130 -0
- package/packages/daemon/dist/widgets/explorer/breadcrumbs.d.ts +1 -1
- package/packages/daemon/dist/widgets/explorer/footer.d.ts +1 -1
- package/packages/daemon/dist/widgets/explorer/tree.d.ts +1 -1
- package/packages/daemon/dist/widgets/lib/help-overlay.d.ts +1 -1
- package/packages/daemon/dist/widgets/setup/agent-naming.d.ts +1 -1
- package/packages/daemon/dist/widgets/setup/config-tree.d.ts +1 -1
- package/packages/daemon/dist/widgets/setup/detect-panel.d.ts +1 -1
- package/packages/daemon/dist/widgets/setup/field-editor.d.ts +1 -1
- package/packages/daemon/dist/widgets/setup/footer.d.ts +1 -1
- package/packages/daemon/dist/widgets/setup/layout-picker.d.ts +1 -1
- package/packages/daemon/src/agent-explain.ts +298 -0
- package/packages/daemon/src/attach.ts +20 -0
- package/packages/daemon/src/bin.ts +4 -0
- package/packages/daemon/src/canonical.ts +7 -0
- package/packages/daemon/src/cli.ts +499 -0
- package/packages/daemon/src/command-center/actions/contract.ts +2 -0
- package/packages/daemon/src/command-center/actions/dispatcher.ts +137 -0
- package/packages/daemon/src/command-center/actions/errors.ts +105 -0
- package/packages/daemon/src/command-center/actions/handlers/_project-context.ts +30 -0
- package/packages/daemon/src/command-center/actions/handlers/_resolve-project.ts +78 -0
- package/packages/daemon/src/command-center/actions/handlers/app-set-remote-access.ts +118 -0
- package/packages/daemon/src/command-center/actions/handlers/config-actions.ts +113 -0
- package/packages/daemon/src/command-center/actions/handlers/daemon-shutdown.ts +38 -0
- package/packages/daemon/src/command-center/actions/handlers/project-activate.ts +30 -0
- package/packages/daemon/src/command-center/actions/handlers/project-launch.ts +70 -0
- package/packages/daemon/src/command-center/actions/handlers/project-open-terminal.ts +87 -0
- package/packages/daemon/src/command-center/actions/handlers/project-restart.ts +38 -0
- package/packages/daemon/src/command-center/actions/handlers/project-stop.ts +62 -0
- package/packages/daemon/src/command-center/actions/handlers/terminal-respawn.ts +119 -0
- package/packages/daemon/src/command-center/actions/handlers/terminal-stop.ts +35 -0
- package/packages/daemon/src/command-center/actions/registry.ts +149 -0
- package/packages/daemon/src/command-center/discovery.ts +96 -0
- package/packages/daemon/src/command-center/index.ts +31 -0
- package/packages/daemon/src/command-center/schemas.ts +85 -0
- package/packages/daemon/src/command-center/server.ts +1260 -0
- package/packages/daemon/src/command-center/ws-events.ts +316 -0
- package/packages/daemon/src/config.ts +549 -0
- package/packages/daemon/src/detect.ts +248 -0
- package/packages/daemon/src/doctor.ts +242 -0
- package/packages/daemon/src/embed.ts +5 -0
- package/packages/daemon/src/index.ts +12 -0
- package/packages/daemon/src/init.ts +211 -0
- package/packages/daemon/src/inspect.ts +178 -0
- package/packages/daemon/src/js-yaml.d.ts +10 -0
- package/packages/daemon/src/launch.ts +349 -0
- package/packages/daemon/src/lib/active-projects.ts +49 -0
- package/packages/daemon/src/lib/agent-discovery.ts +121 -0
- package/packages/daemon/src/lib/app-config.ts +427 -0
- package/packages/daemon/src/lib/app-settings.ts +53 -0
- package/packages/daemon/src/lib/auth/auth-service.ts +227 -0
- package/packages/daemon/src/lib/auth/middleware.ts +56 -0
- package/packages/daemon/src/lib/auth/types.ts +2 -0
- package/packages/daemon/src/lib/auth-token.ts +5 -0
- package/packages/daemon/src/lib/authorship.ts +280 -0
- package/packages/daemon/src/lib/canonical-daemon.ts +122 -0
- package/packages/daemon/src/lib/cli-action-bridge.ts +216 -0
- package/packages/daemon/src/lib/daemon-embed.ts +782 -0
- package/packages/daemon/src/lib/daemon-watchdog.ts +111 -0
- package/packages/daemon/src/lib/daemon.ts +79 -0
- package/packages/daemon/src/lib/dot-path.ts +17 -0
- package/packages/daemon/src/lib/errors.ts +67 -0
- package/packages/daemon/src/lib/filesystem-browser.ts +292 -0
- package/packages/daemon/src/lib/launch-plan.ts +90 -0
- package/packages/daemon/src/lib/log.ts +134 -0
- package/packages/daemon/src/lib/output.ts +76 -0
- package/packages/daemon/src/lib/project-init-runner.ts +150 -0
- package/packages/daemon/src/lib/project-inspect.ts +80 -0
- package/packages/daemon/src/lib/project-onboard.ts +149 -0
- package/packages/daemon/src/lib/project-probe.ts +92 -0
- package/packages/daemon/src/lib/project-registry.ts +296 -0
- package/packages/daemon/src/lib/session-monitor.ts +122 -0
- package/packages/daemon/src/lib/session-options.ts +100 -0
- package/packages/daemon/src/lib/shell.ts +8 -0
- package/packages/daemon/src/lib/sizes.ts +36 -0
- package/packages/daemon/src/lib/skill-sync.ts +155 -0
- package/packages/daemon/src/lib/slugify.ts +10 -0
- package/packages/daemon/src/lib/terminals-store.ts +125 -0
- package/packages/daemon/src/lib/tui-binary.ts +165 -0
- package/packages/daemon/src/lib/update-check.ts +298 -0
- package/packages/daemon/src/lib/update.ts +158 -0
- package/packages/daemon/src/lib/workspace-registry.ts +229 -0
- package/packages/daemon/src/lib/worktree.ts +289 -0
- package/packages/daemon/src/lib/yaml-io.ts +27 -0
- package/packages/daemon/src/ls.ts +40 -0
- package/packages/daemon/src/restart.ts +24 -0
- package/packages/daemon/src/restore.ts +514 -0
- package/packages/daemon/src/schemas/domain.ts +2 -0
- package/packages/daemon/src/schemas/filesystem.ts +34 -0
- package/packages/daemon/src/schemas/ide-config.ts +2 -0
- package/packages/daemon/src/schemas/index.ts +59 -0
- package/packages/daemon/src/schemas/inspect.ts +67 -0
- package/packages/daemon/src/schemas/registry.ts +55 -0
- package/packages/daemon/src/schemas/ws-events.ts +135 -0
- package/packages/daemon/src/send.ts +171 -0
- package/packages/daemon/src/server/README.md +15 -0
- package/packages/daemon/src/server/index.ts +74 -0
- package/packages/daemon/src/server/pty-bridge.ts +532 -0
- package/packages/daemon/src/server/standalone.ts +18 -0
- package/packages/daemon/src/server/ws-route.ts +483 -0
- package/packages/daemon/src/status.ts +59 -0
- package/packages/daemon/src/stop.ts +28 -0
- package/packages/daemon/src/terminal/NodePtyAdapter.ts +271 -0
- package/packages/daemon/src/terminal/PtyAdapter.ts +140 -0
- package/packages/daemon/src/terminal/README.md +92 -0
- package/packages/daemon/src/tui/chrome/cheatsheet.ts +260 -0
- package/packages/daemon/src/tui/chrome/chip.ts +30 -0
- package/packages/daemon/src/tui/chrome/events.ts +119 -0
- package/packages/daemon/src/tui/chrome/kitty-keys.ts +55 -0
- package/packages/daemon/src/tui/chrome/menu.ts +289 -0
- package/packages/daemon/src/tui/chrome/notify.ts +382 -0
- package/packages/daemon/src/tui/chrome/panels.ts +111 -0
- package/packages/daemon/src/tui/chrome/sidebar.ts +222 -0
- package/packages/daemon/src/tui/chrome/snapshot.ts +425 -0
- package/packages/daemon/src/tui/chrome/statusline.ts +595 -0
- package/packages/daemon/src/tui/chrome/updater.ts +510 -0
- package/packages/daemon/src/tui/chrome/welcome.ts +124 -0
- package/packages/daemon/src/tui/compiled.ts +121 -0
- package/packages/daemon/src/tui/detect/classify.ts +208 -0
- package/packages/daemon/src/tui/detect/manifest-loader.ts +193 -0
- package/packages/daemon/src/tui/detect/manifest.ts +199 -0
- package/packages/daemon/src/tui/detect/manifests.ts +354 -0
- package/packages/daemon/src/tui/detect/process-tree.ts +217 -0
- package/packages/daemon/src/tui/detect/snapshot.ts +70 -0
- package/packages/daemon/src/tui/integrations/claude.ts +176 -0
- package/packages/daemon/src/tui/integrations/offer.ts +145 -0
- package/packages/daemon/src/tui/main.ts +82 -0
- package/packages/daemon/src/tui/mirror/ack-writer.ts +77 -0
- package/packages/daemon/src/tui/mirror/agent-chip.ts +97 -0
- package/packages/daemon/src/tui/mirror/agent-rows.ts +133 -0
- package/packages/daemon/src/tui/mirror/app-state.ts +179 -0
- package/packages/daemon/src/tui/mirror/app.tsx +5265 -0
- package/packages/daemon/src/tui/mirror/blit.ts +186 -0
- package/packages/daemon/src/tui/mirror/control-client.ts +214 -0
- package/packages/daemon/src/tui/mirror/control.ts +97 -0
- package/packages/daemon/src/tui/mirror/dialog-model.ts +298 -0
- package/packages/daemon/src/tui/mirror/dialog-stack.ts +354 -0
- package/packages/daemon/src/tui/mirror/diff-model.ts +112 -0
- package/packages/daemon/src/tui/mirror/editor-buffer.ts +117 -0
- package/packages/daemon/src/tui/mirror/file-tree.ts +97 -0
- package/packages/daemon/src/tui/mirror/focus-border.ts +57 -0
- package/packages/daemon/src/tui/mirror/folder-picker.ts +124 -0
- package/packages/daemon/src/tui/mirror/home-model.ts +174 -0
- package/packages/daemon/src/tui/mirror/input-coalescer.ts +105 -0
- package/packages/daemon/src/tui/mirror/menu-model.ts +187 -0
- package/packages/daemon/src/tui/mirror/palette.ts +274 -0
- package/packages/daemon/src/tui/mirror/pane-mirror.ts +561 -0
- package/packages/daemon/src/tui/mirror/pane-surface.tsx +415 -0
- package/packages/daemon/src/tui/mirror/perf-tap.ts +160 -0
- package/packages/daemon/src/tui/mirror/resize-model.ts +85 -0
- package/packages/daemon/src/tui/mirror/scrollbar-model.ts +88 -0
- package/packages/daemon/src/tui/mirror/search-model.ts +70 -0
- package/packages/daemon/src/tui/mirror/selection.ts +262 -0
- package/packages/daemon/src/tui/mirror/session-mirror.ts +443 -0
- package/packages/daemon/src/tui/mirror/settings-model.ts +345 -0
- package/packages/daemon/src/tui/mirror/size-truth.ts +77 -0
- package/packages/daemon/src/tui/mirror/spans.ts +46 -0
- package/packages/daemon/src/tui/mirror/status-grammar.ts +32 -0
- package/packages/daemon/src/tui/team/CONTROL.md +50 -0
- package/packages/daemon/src/tui/team/entry.ts +38 -0
- package/packages/daemon/src/tui/team/fuzzy.ts +133 -0
- package/packages/daemon/src/tui/team/home.ts +170 -0
- package/packages/daemon/src/tui/team/index.tsx +1521 -0
- package/packages/daemon/src/tui/team/input.ts +34 -0
- package/packages/daemon/src/tui/team/keymap.ts +127 -0
- package/packages/daemon/src/tui/team/mouse.ts +29 -0
- package/packages/daemon/src/tui/team/nav.ts +31 -0
- package/packages/daemon/src/tui/team/preview.ts +34 -0
- package/packages/daemon/src/tui/team/projects.ts +191 -0
- package/packages/daemon/src/tui/team/report.ts +83 -0
- package/packages/daemon/src/tui/team/sessions.ts +433 -0
- package/packages/daemon/src/tui/team/tree.ts +62 -0
- package/packages/daemon/src/types.ts +13 -0
- package/packages/daemon/src/ui/index.ts +32 -0
- package/packages/daemon/src/ui/terminal/index.ts +9 -0
- package/packages/daemon/src/ui/types.ts +91 -0
- package/packages/daemon/src/ui/web/base.css +80 -0
- package/packages/daemon/src/ui/web/components/Box.tsx +59 -0
- package/packages/daemon/src/ui/web/components/Input.tsx +32 -0
- package/packages/daemon/src/ui/web/components/ScrollBox.tsx +60 -0
- package/packages/daemon/src/ui/web/components/Text.tsx +28 -0
- package/packages/daemon/src/ui/web/hooks.ts +106 -0
- package/packages/daemon/src/ui/web/index.ts +27 -0
- package/packages/daemon/src/ui/web/render.ts +77 -0
- package/packages/daemon/src/ui/web/utils/color.ts +27 -0
- package/packages/daemon/src/validate.ts +217 -0
- package/packages/daemon/src/widgets/changes/README.md +3 -0
- package/packages/daemon/src/widgets/changes/index.tsx +691 -0
- package/packages/daemon/src/widgets/config/README.md +3 -0
- package/packages/daemon/src/widgets/config/index.tsx +481 -0
- package/packages/daemon/src/widgets/explorer/README.md +3 -0
- package/packages/daemon/src/widgets/explorer/breadcrumbs.tsx +77 -0
- package/packages/daemon/src/widgets/explorer/footer.tsx +20 -0
- package/packages/daemon/src/widgets/explorer/header.tsx +23 -0
- package/packages/daemon/src/widgets/explorer/index.tsx +456 -0
- package/packages/daemon/src/widgets/explorer/tree-model.ts +103 -0
- package/packages/daemon/src/widgets/explorer/tree.tsx +165 -0
- package/packages/daemon/src/widgets/lib/config-model.ts +116 -0
- package/packages/daemon/src/widgets/lib/files.ts +88 -0
- package/packages/daemon/src/widgets/lib/git.ts +88 -0
- package/packages/daemon/src/widgets/lib/grammar.ts +126 -0
- package/packages/daemon/src/widgets/lib/help-overlay.tsx +101 -0
- package/packages/daemon/src/widgets/lib/pane-comms.ts +209 -0
- package/packages/daemon/src/widgets/lib/theme.ts +194 -0
- package/packages/daemon/src/widgets/lib/watcher.ts +132 -0
- package/packages/daemon/src/widgets/preview/README.md +3 -0
- package/packages/daemon/src/widgets/preview/index.tsx +416 -0
- package/packages/daemon/src/widgets/resolve.ts +121 -0
- package/packages/daemon/src/widgets/setup/README.md +3 -0
- package/packages/daemon/src/widgets/setup/agent-naming.tsx +112 -0
- package/packages/daemon/src/widgets/setup/config-tree.tsx +246 -0
- package/packages/daemon/src/widgets/setup/detect-panel.tsx +72 -0
- package/packages/daemon/src/widgets/setup/field-editor.tsx +265 -0
- package/packages/daemon/src/widgets/setup/footer.tsx +107 -0
- package/packages/daemon/src/widgets/setup/index.tsx +341 -0
- package/packages/daemon/src/widgets/setup/layout-picker.tsx +96 -0
- package/packages/daemon/src/widgets/setup/orchestrator-panel.tsx +200 -0
- package/packages/daemon/src/widgets/setup/review-panel.tsx +140 -0
- package/packages/daemon/src/widgets/setup/setup-model.ts +188 -0
- package/packages/daemon/src/widgets/sidebar/index.tsx +527 -0
- package/packages/tmux-bridge/package.json +22 -0
- package/packages/tmux-bridge/src/errors.ts +28 -0
- package/packages/tmux-bridge/src/index.ts +31 -0
- package/packages/tmux-bridge/src/monitor.ts +77 -0
- package/packages/tmux-bridge/src/panes.ts +136 -0
- package/packages/tmux-bridge/src/runner.test.ts +501 -0
- package/packages/tmux-bridge/src/runner.ts +91 -0
- package/packages/tmux-bridge/src/sessions.ts +126 -0
- package/packages/tmux-bridge/src/targeting.test.ts +107 -0
- package/packages/tmux-bridge/src/targeting.ts +90 -0
- package/scripts/build-tui.mjs +11 -4
- package/scripts/perf-mirror.mjs +313 -0
- package/scripts/postinstall.js +26 -2
- package/skill/SKILL.md +22 -0
- package/templates/AGENTS.md +14 -7
- package/templates/agent-team-monorepo.yml +8 -0
- package/templates/agent-team-nextjs.yml +8 -0
- package/templates/agent-team.yml +10 -0
- package/templates/convex.yml +2 -0
- package/templates/default.yml +11 -5
- package/templates/go.yml +4 -0
- package/templates/missions.yml +6 -0
- package/templates/nextjs.yml +4 -0
- package/templates/python.yml +4 -0
- package/templates/skills/backend.md +5 -12
- package/templates/skills/frontend.md +5 -12
- package/templates/skills/general-worker.md +5 -12
- package/templates/skills/researcher.md +7 -12
- package/templates/skills/reviewer.md +7 -16
- package/templates/vite.yml +4 -0
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure scrollbar geometry (M19.5) — shared by the editor viewport, the diff
|
|
3
|
+
* pane, and any scrolled mirror pane.
|
|
4
|
+
*
|
|
5
|
+
* A 1-col track at the right edge of an always-present container; the thumb's
|
|
6
|
+
* position and size are pure functions of (viewportTop, contentLen, viewH).
|
|
7
|
+
* Both the RENDER (draws the track cells) and the ROUTER (classifies a press as
|
|
8
|
+
* thumb vs track, maps a thumb drag to an absolute scroll top, pages a track
|
|
9
|
+
* click) read the SAME math from here — the surface-bar discipline applied to a
|
|
10
|
+
* vertical strip. Deliberately opentui-free so it unit-tests under Node.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export interface ScrollThumb {
|
|
14
|
+
/** Whether the content overflows the viewport (nothing to scroll → hidden). */
|
|
15
|
+
overflow: boolean;
|
|
16
|
+
/** First track row (0-based, within [0, viewH)) the thumb occupies. */
|
|
17
|
+
start: number;
|
|
18
|
+
/** Thumb height in rows (≥1, ≤viewH). */
|
|
19
|
+
size: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** The thumb size for a content/viewport pair — a viewport-fraction of the
|
|
23
|
+
* track, floored at one cell so a thumb always exists. Shared by every
|
|
24
|
+
* function below so their maths never drift. */
|
|
25
|
+
function thumbSize(contentLen: number, viewH: number): number {
|
|
26
|
+
return Math.max(1, Math.min(viewH, Math.round((viewH * viewH) / contentLen)));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** The thumb geometry for a scroll position. `viewportTop` is the first visible
|
|
30
|
+
* content line; `contentLen` the total lines; `viewH` the visible rows. When
|
|
31
|
+
* the content fits, `overflow` is false and the caller draws no track. */
|
|
32
|
+
export function scrollThumb(viewportTop: number, contentLen: number, viewH: number): ScrollThumb {
|
|
33
|
+
if (viewH <= 0 || contentLen <= viewH) {
|
|
34
|
+
return { overflow: false, start: 0, size: Math.max(1, viewH) };
|
|
35
|
+
}
|
|
36
|
+
const size = thumbSize(contentLen, viewH);
|
|
37
|
+
const maxTop = contentLen - viewH;
|
|
38
|
+
const maxStart = viewH - size;
|
|
39
|
+
const top = Math.max(0, Math.min(maxTop, viewportTop));
|
|
40
|
+
const start = maxStart <= 0 ? 0 : Math.round((top / maxTop) * maxStart);
|
|
41
|
+
return { overflow: true, start: Math.max(0, Math.min(maxStart, start)), size };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Where a track row falls relative to the thumb — the fourth drag-origin's
|
|
45
|
+
* press classifier and the page-click direction source. */
|
|
46
|
+
export type TrackZone = "above" | "thumb" | "below";
|
|
47
|
+
|
|
48
|
+
export function trackZone(row: number, thumb: ScrollThumb): TrackZone {
|
|
49
|
+
if (row < thumb.start) return "above";
|
|
50
|
+
if (row >= thumb.start + thumb.size) return "below";
|
|
51
|
+
return "thumb";
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Page the viewport toward a click at track `row` (above the thumb → up one
|
|
55
|
+
* viewport, below → down one; on the thumb → unchanged), clamped to range. */
|
|
56
|
+
export function pageTop(
|
|
57
|
+
row: number,
|
|
58
|
+
viewportTop: number,
|
|
59
|
+
contentLen: number,
|
|
60
|
+
viewH: number,
|
|
61
|
+
): number {
|
|
62
|
+
const thumb = scrollThumb(viewportTop, contentLen, viewH);
|
|
63
|
+
if (!thumb.overflow) return viewportTop;
|
|
64
|
+
const maxTop = contentLen - viewH;
|
|
65
|
+
const zone = trackZone(row, thumb);
|
|
66
|
+
const next =
|
|
67
|
+
zone === "above" ? viewportTop - viewH : zone === "below" ? viewportTop + viewH : viewportTop;
|
|
68
|
+
return Math.max(0, Math.min(maxTop, next));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Map a dragged thumb to an absolute scroll top. `trackRow` is the current
|
|
72
|
+
* pointer row within the track; `grabOffset` is the row within the thumb where
|
|
73
|
+
* the drag began (so the thumb doesn't jump under the cursor). Returns the new
|
|
74
|
+
* first-visible line, clamped. */
|
|
75
|
+
export function dragTop(
|
|
76
|
+
trackRow: number,
|
|
77
|
+
grabOffset: number,
|
|
78
|
+
contentLen: number,
|
|
79
|
+
viewH: number,
|
|
80
|
+
): number {
|
|
81
|
+
if (viewH <= 0 || contentLen <= viewH) return 0;
|
|
82
|
+
const size = thumbSize(contentLen, viewH);
|
|
83
|
+
const maxStart = viewH - size;
|
|
84
|
+
const maxTop = contentLen - viewH;
|
|
85
|
+
if (maxStart <= 0) return 0;
|
|
86
|
+
const start = Math.max(0, Math.min(maxStart, trackRow - grabOffset));
|
|
87
|
+
return Math.round((start / maxStart) * maxTop);
|
|
88
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure, io-free math for scrollback search (M20.3) — the copy-mode `/` finder,
|
|
3
|
+
* app-native. app.tsx owns the search-session state machine + the io (reading a
|
|
4
|
+
* pane's `bufferLines`, driving scrollOffset); this module owns the arithmetic,
|
|
5
|
+
* unit-tested without a terminal exactly like selection.ts / diff-model.ts.
|
|
6
|
+
*
|
|
7
|
+
* A match is a (line, col) into the pane's full buffer (0 = oldest scrollback
|
|
8
|
+
* line). The render maps a match line to a visible row and inverse/accent-tints
|
|
9
|
+
* its [col, col+len) span; a jump converts a match line to a scrollOffset.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/** One substring hit: `line` is the absolute buffer line, `col` the 0-based
|
|
13
|
+
* character column of the match start on that line. */
|
|
14
|
+
export interface SearchMatch {
|
|
15
|
+
line: number;
|
|
16
|
+
col: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Every case-insensitive substring occurrence of `query` across `lines`, in
|
|
21
|
+
* buffer order (top→bottom, left→right), one entry per occurrence (multiple per
|
|
22
|
+
* line allowed; non-overlapping — the scan advances past each hit). An empty
|
|
23
|
+
* query yields no matches.
|
|
24
|
+
*/
|
|
25
|
+
export function findMatches(lines: readonly string[], query: string): SearchMatch[] {
|
|
26
|
+
const out: SearchMatch[] = [];
|
|
27
|
+
if (query.length === 0) return out;
|
|
28
|
+
const needle = query.toLowerCase();
|
|
29
|
+
for (let line = 0; line < lines.length; line++) {
|
|
30
|
+
const hay = (lines[line] ?? "").toLowerCase();
|
|
31
|
+
let from = 0;
|
|
32
|
+
for (;;) {
|
|
33
|
+
const idx = hay.indexOf(needle, from);
|
|
34
|
+
if (idx === -1) break;
|
|
35
|
+
out.push({ line, col: idx });
|
|
36
|
+
from = idx + needle.length;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return out;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Matches in VISIT order — bottom-up (nearest the live viewport first), the
|
|
44
|
+
* order `/`-then-Enter lands on and n/N cycle through. So visit index 0 is the
|
|
45
|
+
* bottom-most match (shown as "1/N"), and `n` walks upward through the buffer.
|
|
46
|
+
* A fresh copy (does not mutate the input).
|
|
47
|
+
*/
|
|
48
|
+
export function visitOrder(matches: readonly SearchMatch[]): SearchMatch[] {
|
|
49
|
+
return [...matches].reverse();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Cycle a match index by `dir` (+1 next / -1 prev) with wraparound; -1 stays
|
|
53
|
+
* -1 when there are no matches. */
|
|
54
|
+
export function stepMatch(current: number, dir: number, total: number): number {
|
|
55
|
+
if (total <= 0) return -1;
|
|
56
|
+
return (((current + dir) % total) + total) % total;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* The scrollOffset (lines above the live viewport) that brings buffer `line`
|
|
61
|
+
* into view, positioned roughly at the middle of a `viewH`-row viewport, clamped
|
|
62
|
+
* to [0, depth]. `depth` is the scrollback budget (lines above the live top);
|
|
63
|
+
* the pane snapshot renders visible row `r` from buffer line `depth - offset + r`,
|
|
64
|
+
* so putting `line` at row `target` needs `offset = depth + target - line`.
|
|
65
|
+
*/
|
|
66
|
+
export function offsetForMatch(line: number, depth: number, viewH: number): number {
|
|
67
|
+
const target = Math.floor(Math.max(0, viewH) / 2);
|
|
68
|
+
const raw = depth + target - line;
|
|
69
|
+
return Math.max(0, Math.min(depth, raw));
|
|
70
|
+
}
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure, io-free geometry + text math for mouse selection & clipboard (M19.4).
|
|
3
|
+
*
|
|
4
|
+
* Two surfaces select the same way — the session MIRROR (pane snapshot rows)
|
|
5
|
+
* and the built-in EDITOR (buffer lines) — so the coordinate math lives here,
|
|
6
|
+
* unit-tested without a terminal, exactly like spans.ts / diff-model.ts. app.tsx
|
|
7
|
+
* owns the mouse-gesture state machine and the io (OSC52 to stdout, tmux
|
|
8
|
+
* passthrough, send-keys); this module owns the arithmetic.
|
|
9
|
+
*
|
|
10
|
+
* The selection is LINEAR (terminal-style, reading order), not rectangular: a
|
|
11
|
+
* cell is selected if it falls between the anchor and head in row-major order.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/** A cell in surface-local coordinates: mirror = snapshot row + pane column;
|
|
15
|
+
* editor = buffer line + buffer column. */
|
|
16
|
+
export interface Cell {
|
|
17
|
+
row: number;
|
|
18
|
+
col: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** An active selection on one surface. `anchor` is where the drag began, `head`
|
|
22
|
+
* the current/last pointer cell; either may precede the other. */
|
|
23
|
+
export type Selection =
|
|
24
|
+
| { surface: "mirror"; paneId: string; anchor: Cell; head: Cell }
|
|
25
|
+
| { surface: "editor"; anchor: Cell; head: Cell };
|
|
26
|
+
|
|
27
|
+
/** OpenTUI TextAttributes bit for inverse video (matches pane-mirror.ts). */
|
|
28
|
+
export const ATTR_INVERSE = 32;
|
|
29
|
+
|
|
30
|
+
/** Order two cells into reading order (row-major); both endpoints inclusive. */
|
|
31
|
+
export function orderCells(a: Cell, b: Cell): { start: Cell; end: Cell } {
|
|
32
|
+
if (a.row < b.row || (a.row === b.row && a.col <= b.col)) return { start: a, end: b };
|
|
33
|
+
return { start: b, end: a };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The inclusive [from,to] column interval selected on `row` for a linear
|
|
38
|
+
* selection between ordered endpoints, or null when the row is outside the
|
|
39
|
+
* selection or has no cells. `rowLen` is the row's character count.
|
|
40
|
+
*/
|
|
41
|
+
export function rowSelectionRange(
|
|
42
|
+
row: number,
|
|
43
|
+
rowLen: number,
|
|
44
|
+
start: Cell,
|
|
45
|
+
end: Cell,
|
|
46
|
+
): { from: number; to: number } | null {
|
|
47
|
+
if (row < start.row || row > end.row) return null;
|
|
48
|
+
if (rowLen <= 0) return null;
|
|
49
|
+
const rawFrom = row === start.row ? start.col : 0;
|
|
50
|
+
const rawTo = row === end.row ? end.col : rowLen - 1;
|
|
51
|
+
const from = Math.max(0, Math.min(rawFrom, rowLen - 1));
|
|
52
|
+
const to = Math.max(0, Math.min(rawTo, rowLen - 1));
|
|
53
|
+
if (to < from) return null;
|
|
54
|
+
return { from, to };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Extract the selected text from per-row plain strings between ordered
|
|
59
|
+
* endpoints. Rows join with "\n". When `trimTrailing` (mirror snapshot rows are
|
|
60
|
+
* space-padded to the pane width), trailing whitespace on each visual row is
|
|
61
|
+
* stripped — the terminal-copy convention; the editor passes false to keep
|
|
62
|
+
* buffer text exact.
|
|
63
|
+
*/
|
|
64
|
+
export function extractSelection(
|
|
65
|
+
rowTexts: string[],
|
|
66
|
+
start: Cell,
|
|
67
|
+
end: Cell,
|
|
68
|
+
trimTrailing = true,
|
|
69
|
+
): string {
|
|
70
|
+
const out: string[] = [];
|
|
71
|
+
for (let r = start.row; r <= end.row; r++) {
|
|
72
|
+
const text = rowTexts[r] ?? "";
|
|
73
|
+
const range = rowSelectionRange(r, text.length, start, end);
|
|
74
|
+
let seg = range ? text.slice(range.from, range.to + 1) : "";
|
|
75
|
+
if (trimTrailing) seg = seg.replace(/\s+$/u, "");
|
|
76
|
+
out.push(seg);
|
|
77
|
+
}
|
|
78
|
+
return out.join("\n");
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* The inclusive [from,to] char interval of the word at `col` in `text`,
|
|
83
|
+
* splitting on /\W/ (word chars are [A-Za-z0-9_]). A click on a non-word char
|
|
84
|
+
* selects just that char; an empty line selects {0,0}.
|
|
85
|
+
*/
|
|
86
|
+
export function wordRangeAt(text: string, col: number): { from: number; to: number } {
|
|
87
|
+
const n = text.length;
|
|
88
|
+
if (n === 0) return { from: 0, to: 0 };
|
|
89
|
+
const c = Math.max(0, Math.min(col, n - 1));
|
|
90
|
+
const isWord = (ch: string): boolean => /\w/u.test(ch);
|
|
91
|
+
if (!isWord(text[c]!)) return { from: c, to: c };
|
|
92
|
+
let from = c;
|
|
93
|
+
let to = c;
|
|
94
|
+
while (from > 0 && isWord(text[from - 1]!)) from--;
|
|
95
|
+
while (to < n - 1 && isWord(text[to + 1]!)) to++;
|
|
96
|
+
return { from, to };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** The inclusive [from,to] interval covering the line up to its last non-blank
|
|
100
|
+
* char (a blank line → {0,0}). */
|
|
101
|
+
export function lineRangeAt(text: string): { from: number; to: number } {
|
|
102
|
+
const trimmed = text.replace(/\s+$/u, "");
|
|
103
|
+
if (trimmed.length === 0) return { from: 0, to: 0 };
|
|
104
|
+
return { from: 0, to: trimmed.length - 1 };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* The resulting click count (1/2/3, cycling to 1 after 3) for double/triple
|
|
109
|
+
* detection: a click at the same cell within `windowMs` of the previous one
|
|
110
|
+
* increments; anything else resets to 1.
|
|
111
|
+
*/
|
|
112
|
+
export function clickCount(
|
|
113
|
+
prev: { row: number; col: number; ts: number; count: number } | null,
|
|
114
|
+
cell: Cell,
|
|
115
|
+
ts: number,
|
|
116
|
+
windowMs: number,
|
|
117
|
+
): number {
|
|
118
|
+
if (prev && prev.row === cell.row && prev.col === cell.col && ts - prev.ts <= windowMs) {
|
|
119
|
+
return prev.count >= 3 ? 1 : prev.count + 1;
|
|
120
|
+
}
|
|
121
|
+
return 1;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Toggle the inverse attribute across chars [from,to] (inclusive, in row-char
|
|
126
|
+
* columns) of a row's styled runs, splitting runs at the boundaries so the
|
|
127
|
+
* selected span renders highlighted while every other run keeps its colors.
|
|
128
|
+
* XOR (not OR) so an already-inverse cell — e.g. the painted cursor — flips
|
|
129
|
+
* back, matching pane-mirror's cursor treatment.
|
|
130
|
+
*/
|
|
131
|
+
export function tintRunsInverse<T extends { text: string; attributes: number }>(
|
|
132
|
+
runs: T[],
|
|
133
|
+
from: number,
|
|
134
|
+
to: number,
|
|
135
|
+
): T[] {
|
|
136
|
+
if (to < from) return runs;
|
|
137
|
+
const out: T[] = [];
|
|
138
|
+
let col = 0;
|
|
139
|
+
for (const run of runs) {
|
|
140
|
+
const len = run.text.length;
|
|
141
|
+
const runStart = col;
|
|
142
|
+
const runEnd = col + len - 1;
|
|
143
|
+
col += len;
|
|
144
|
+
if (len === 0 || runEnd < from || runStart > to) {
|
|
145
|
+
out.push(run);
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
const a = Math.max(from, runStart) - runStart;
|
|
149
|
+
const b = Math.min(to, runEnd) - runStart;
|
|
150
|
+
if (a > 0) out.push({ ...run, text: run.text.slice(0, a) });
|
|
151
|
+
out.push({ ...run, text: run.text.slice(a, b + 1), attributes: run.attributes ^ ATTR_INVERSE });
|
|
152
|
+
if (b + 1 < len) out.push({ ...run, text: run.text.slice(b + 1) });
|
|
153
|
+
}
|
|
154
|
+
return out;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Set the background color across chars [from,to] (inclusive, in row-char
|
|
159
|
+
* columns) of a row's styled runs, splitting runs at the boundaries so the
|
|
160
|
+
* spanned cells render with `bg` while every other run keeps its colors. The
|
|
161
|
+
* twin of {@link tintRunsInverse} — search-match highlighting paints an accent
|
|
162
|
+
* background (a distinct treatment from the inverse-video selection) so the two
|
|
163
|
+
* can coexist on one row. `bg` is packed 0xRRGGBB (the run bg convention).
|
|
164
|
+
*/
|
|
165
|
+
export function tintRunsBg<T extends { text: string; bg: number | null }>(
|
|
166
|
+
runs: T[],
|
|
167
|
+
from: number,
|
|
168
|
+
to: number,
|
|
169
|
+
bg: number,
|
|
170
|
+
): T[] {
|
|
171
|
+
if (to < from) return runs;
|
|
172
|
+
const out: T[] = [];
|
|
173
|
+
let col = 0;
|
|
174
|
+
for (const run of runs) {
|
|
175
|
+
const len = run.text.length;
|
|
176
|
+
const runStart = col;
|
|
177
|
+
const runEnd = col + len - 1;
|
|
178
|
+
col += len;
|
|
179
|
+
if (len === 0 || runEnd < from || runStart > to) {
|
|
180
|
+
out.push(run);
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
const a = Math.max(from, runStart) - runStart;
|
|
184
|
+
const b = Math.min(to, runEnd) - runStart;
|
|
185
|
+
if (a > 0) out.push({ ...run, text: run.text.slice(0, a) });
|
|
186
|
+
out.push({ ...run, text: run.text.slice(a, b + 1), bg });
|
|
187
|
+
if (b + 1 < len) out.push({ ...run, text: run.text.slice(b + 1) });
|
|
188
|
+
}
|
|
189
|
+
return out;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// ── Select mode on app-mouse panes (M22.9) ──────────────────────────────────
|
|
193
|
+
// Panes whose app turned mouse reporting on (`appMouse`) normally get every
|
|
194
|
+
// press FORWARDED, so a drag can never start a local selection there. Two
|
|
195
|
+
// entries reopen selection: an explicit per-pane SELECT MODE (right-click →
|
|
196
|
+
// "Select text…", paused forwarding until esc / a completed copy / focus
|
|
197
|
+
// leaving the pane) and a SHIFT-modified press when the terminal passes shift
|
|
198
|
+
// through (SGR encodes it as +4 on the button code; many terminals keep
|
|
199
|
+
// shift+drag for native selection instead — then only select mode applies).
|
|
200
|
+
|
|
201
|
+
/** PURE — whether a left press on a pane begins a LOCAL drag selection
|
|
202
|
+
* (vs. being forwarded to the pane's app as an SGR press). */
|
|
203
|
+
export function pressStartsSelection(
|
|
204
|
+
appMouse: boolean,
|
|
205
|
+
selectModeOn: boolean,
|
|
206
|
+
shift: boolean,
|
|
207
|
+
): boolean {
|
|
208
|
+
return !appMouse || selectModeOn || shift;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** PURE — whether the wheel scrolls the LOCAL mirror scrollback (vs. being
|
|
212
|
+
* forwarded as SGR wheel events). Select mode reclaims the wheel so older
|
|
213
|
+
* output can be scrolled into view and selected. */
|
|
214
|
+
export function wheelScrollsLocal(appMouse: boolean, selectModeOn: boolean): boolean {
|
|
215
|
+
return !appMouse || selectModeOn;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** PURE — the select-mode badge text for a pane width (rendered in the pane's
|
|
219
|
+
* top-right badge family, next to the scroll badge), degrading label → glyph
|
|
220
|
+
* → hidden on narrow panes exactly like the agent chip's budget. */
|
|
221
|
+
export function selectBadgeLabel(paneWidth: number): string | null {
|
|
222
|
+
if (paneWidth >= 16) return " ⧉ select ";
|
|
223
|
+
if (paneWidth >= 5) return " ⧉ ";
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** The raw OSC52 clipboard-set escape for a base64 payload (BEL-terminated). */
|
|
228
|
+
export function osc52Sequence(base64: string): string {
|
|
229
|
+
return `\x1b]52;c;${base64}\x07`;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Wrap a sequence in the tmux passthrough envelope (DCS `tmux;` … ST) with inner
|
|
234
|
+
* ESC bytes doubled, so it rides through an outer tmux (with allow-passthrough
|
|
235
|
+
* on) verbatim to the real terminal. Kept tested + available; the app prefers
|
|
236
|
+
* raw OSC52 so `set-clipboard on` captures it into tmux's paste buffer.
|
|
237
|
+
*/
|
|
238
|
+
export function tmuxPassthrough(seq: string): string {
|
|
239
|
+
const doubled = seq.split("\x1b").join("\x1b\x1b");
|
|
240
|
+
return `\x1bPtmux;${doubled}\x1b\\`;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/** Split `text` into chunks each at most `maxBytes` UTF-8 bytes, never breaking
|
|
244
|
+
* a code point — for send-keys -H, whose per-command length tmux caps. */
|
|
245
|
+
export function chunkByBytes(text: string, maxBytes: number): string[] {
|
|
246
|
+
const enc = new TextEncoder();
|
|
247
|
+
const chunks: string[] = [];
|
|
248
|
+
let cur = "";
|
|
249
|
+
let curBytes = 0;
|
|
250
|
+
for (const ch of text) {
|
|
251
|
+
const b = enc.encode(ch).length;
|
|
252
|
+
if (curBytes + b > maxBytes && cur.length > 0) {
|
|
253
|
+
chunks.push(cur);
|
|
254
|
+
cur = "";
|
|
255
|
+
curBytes = 0;
|
|
256
|
+
}
|
|
257
|
+
cur += ch;
|
|
258
|
+
curBytes += b;
|
|
259
|
+
}
|
|
260
|
+
if (cur.length > 0) chunks.push(cur);
|
|
261
|
+
return chunks;
|
|
262
|
+
}
|