commandmate 0.18.0 → 0.19.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/.next/BUILD_ID +1 -1
- package/.next/app-build-manifest.json +131 -131
- package/.next/app-path-routes-manifest.json +23 -23
- package/.next/build-manifest.json +2 -2
- package/.next/prerender-manifest.json +9 -9
- package/.next/required-server-files.json +1 -1
- package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/server/app/api/app/update/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/app/update-check/route.js +1 -1
- package/.next/server/app/api/app/update-check/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/assistant/conversation/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/assistant/current-output/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/assistant/messages/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/assistant/session/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/assistant/start/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/assistant/terminal/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/assistant/tools/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/auth/login/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/auth/logout/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/auth/status/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/daily-summary/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/daily-summary/status/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/external-apps/[id]/health/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/external-apps/[id]/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/external-apps/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/fs/browse/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/fs/recent-paths/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/hooks/agent-event/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/hooks/claude-done/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/metrics/vibe/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/ollama/models/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/push/subscriptions/route.js +1 -1
- package/.next/server/app/api/push/subscriptions/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/push/vapid/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/repositories/[id]/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/repositories/[id]/todos/[todoId]/route.js +1 -1
- package/.next/server/app/api/repositories/[id]/todos/[todoId]/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/repositories/[id]/todos/route.js +1 -1
- package/.next/server/app/api/repositories/[id]/todos/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/repositories/clone/[jobId]/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/repositories/clone/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/repositories/excluded/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/repositories/restore/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/repositories/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/repositories/scan/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/repositories/sync/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/repositories/validate-path/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/sidebar/group-order/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/skills/[id]/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/skills/installations/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/skills/operations/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/skills/reindex/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/skills/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/slash-commands/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/tasks/[taskId]/cancel/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/tasks/[taskId]/route.js +1 -1
- package/.next/server/app/api/tasks/[taskId]/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/templates/[id]/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/templates/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/todos/route.js +1 -1
- package/.next/server/app/api/todos/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/verification/runs/[runId]/route.js +1 -1
- package/.next/server/app/api/verification/runs/[runId]/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/verification/runs/route.js +1 -1
- package/.next/server/app/api/verification/runs/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/auto-yes/route.js +1 -1
- package/.next/server/app/api/worktrees/[id]/auto-yes/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/capture/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/cli-tool/route.js +1 -1
- package/.next/server/app/api/worktrees/[id]/cli-tool/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/cmate/schedules/route.js +1 -1
- package/.next/server/app/api/worktrees/[id]/cmate/schedules/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/current-output/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/execution-logs/[logId]/route.js +1 -1
- package/.next/server/app/api/worktrees/[id]/execution-logs/[logId]/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/execution-logs/route.js +1 -1
- package/.next/server/app/api/worktrees/[id]/execution-logs/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/files/[...path]/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/git/branch/create/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/git/branch/delete/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/git/branches/route.js +1 -1
- package/.next/server/app/api/worktrees/[id]/git/branches/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/git/checkout/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/git/commit/route.js +1 -1
- package/.next/server/app/api/worktrees/[id]/git/commit/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/git/diff/route.js +1 -1
- package/.next/server/app/api/worktrees/[id]/git/diff/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/git/fetch/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/git/log/route.js +1 -1
- package/.next/server/app/api/worktrees/[id]/git/log/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/git/pull/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/git/push/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/git/reset/route.js +1 -1
- package/.next/server/app/api/worktrees/[id]/git/reset/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/git/revert/route.js +1 -1
- package/.next/server/app/api/worktrees/[id]/git/revert/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/git/show/[commitHash]/route.js +1 -1
- package/.next/server/app/api/worktrees/[id]/git/show/[commitHash]/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/git/stage/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/git/staged/route.js +1 -1
- package/.next/server/app/api/worktrees/[id]/git/staged/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/git/stash/[index]/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/git/stash/apply/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/git/stash/pop/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/git/stash/push/route.js +1 -1
- package/.next/server/app/api/worktrees/[id]/git/stash/push/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/git/stash/route.js +1 -1
- package/.next/server/app/api/worktrees/[id]/git/stash/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/git/status/route.js +1 -1
- package/.next/server/app/api/worktrees/[id]/git/status/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/git/unstage/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/git/working-diff/route.js +1 -1
- package/.next/server/app/api/worktrees/[id]/git/working-diff/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/interrupt/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/kill-session/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/logs/[filename]/route.js +1 -1
- package/.next/server/app/api/worktrees/[id]/logs/[filename]/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/logs/route.js +1 -1
- package/.next/server/app/api/worktrees/[id]/logs/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/marp-render/route.js +1 -1
- package/.next/server/app/api/worktrees/[id]/marp-render/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/memos/[memoId]/route.js +1 -1
- package/.next/server/app/api/worktrees/[id]/memos/[memoId]/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/memos/route.js +1 -1
- package/.next/server/app/api/worktrees/[id]/memos/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/messages/route.js +1 -1
- package/.next/server/app/api/worktrees/[id]/messages/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/prompt-response/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/respond/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/schedules/[scheduleId]/route.js +1 -1
- package/.next/server/app/api/worktrees/[id]/schedules/[scheduleId]/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/schedules/active/route.js +1 -1
- package/.next/server/app/api/worktrees/[id]/schedules/active/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/schedules/route.js +1 -1
- package/.next/server/app/api/worktrees/[id]/schedules/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/search/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/send/route.js +1 -1
- package/.next/server/app/api/worktrees/[id]/send/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/skills/[skillId]/git-workflow/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/skills/[skillId]/install/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/skills/[skillId]/plan/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/skills/[skillId]/uninstall/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/skills/[skillId]/uninstall-plan/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/skills/route.js +1 -1
- package/.next/server/app/api/worktrees/[id]/skills/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/slash-commands/route.js +1 -1
- package/.next/server/app/api/worktrees/[id]/slash-commands/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/special-keys/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/start-polling/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/tasks/route.js +2 -2
- package/.next/server/app/api/worktrees/[id]/tasks/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/terminal/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/timers/history/route.js +1 -1
- package/.next/server/app/api/worktrees/[id]/timers/history/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/timers/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/todos/[todoId]/route.js +1 -1
- package/.next/server/app/api/worktrees/[id]/todos/[todoId]/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/todos/route.js +1 -1
- package/.next/server/app/api/worktrees/[id]/todos/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/tree/[...path]/route.js +1 -1
- package/.next/server/app/api/worktrees/[id]/tree/[...path]/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/tree/route.js +1 -1
- package/.next/server/app/api/worktrees/[id]/tree/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/upload/[...path]/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/verify/route.js +1 -1
- package/.next/server/app/api/worktrees/[id]/verify/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/verify/runs/[runId]/route.js +1 -1
- package/.next/server/app/api/worktrees/[id]/verify/runs/[runId]/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/verify/runs/route.js +1 -1
- package/.next/server/app/api/worktrees/[id]/verify/runs/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/viewed/route.js +1 -1
- package/.next/server/app/api/worktrees/[id]/viewed/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/route_client-reference-manifest.js +1 -1
- package/.next/server/app/chat/page_client-reference-manifest.js +1 -1
- package/.next/server/app/login/page_client-reference-manifest.js +1 -1
- package/.next/server/app/manifest.webmanifest/route_client-reference-manifest.js +1 -1
- package/.next/server/app/more/page_client-reference-manifest.js +1 -1
- package/.next/server/app/offline/page_client-reference-manifest.js +1 -1
- package/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/server/app/proxy/[...path]/route_client-reference-manifest.js +1 -1
- package/.next/server/app/repositories/page_client-reference-manifest.js +1 -1
- package/.next/server/app/review/page_client-reference-manifest.js +1 -1
- package/.next/server/app/sessions/page_client-reference-manifest.js +1 -1
- package/.next/server/app/skills/[skillId]/page_client-reference-manifest.js +1 -1
- package/.next/server/app/skills/installed/page_client-reference-manifest.js +1 -1
- package/.next/server/app/skills/page_client-reference-manifest.js +1 -1
- package/.next/server/app/worktrees/[id]/files/[...path]/page_client-reference-manifest.js +1 -1
- package/.next/server/app/worktrees/[id]/page.js +1 -1
- package/.next/server/app/worktrees/[id]/page_client-reference-manifest.js +1 -1
- package/.next/server/app/worktrees/[id]/terminal/page_client-reference-manifest.js +1 -1
- package/.next/server/app-paths-manifest.json +23 -23
- package/.next/server/chunks/1117.js +1 -1
- package/.next/server/chunks/3042.js +1 -1
- package/.next/server/chunks/3753.js +19 -23
- package/.next/server/chunks/5959.js +3 -3
- package/.next/server/chunks/6130.js +1 -1
- package/.next/server/chunks/6274.js +5 -4
- package/.next/server/chunks/6283.js +1 -1
- package/.next/server/chunks/8681.js +1 -1
- package/.next/server/chunks/8807.js +492 -487
- package/.next/server/chunks/8856.js +1 -1
- package/.next/server/middleware-manifest.json +5 -5
- package/.next/server/pages/500.html +1 -1
- package/.next/server/server-reference-manifest.json +1 -1
- package/.next/static/chunks/{6488-109e3825c7249e47.js → 6488-2c412c29258637b6.js} +1 -1
- package/.next/static/chunks/app/worktrees/[id]/{page-12b3c5f32fc6094e.js → page-f6e09d25cd3b56f9.js} +1 -1
- package/.next/trace +12 -12
- package/dist/cli/commands/auto-yes.d.ts.map +1 -1
- package/dist/cli/commands/auto-yes.js +9 -2
- package/dist/cli/commands/capture.d.ts +1 -0
- package/dist/cli/commands/capture.d.ts.map +1 -1
- package/dist/cli/commands/capture.js +112 -2
- package/dist/cli/commands/instances.d.ts +24 -0
- package/dist/cli/commands/instances.d.ts.map +1 -1
- package/dist/cli/commands/instances.js +44 -0
- package/dist/cli/commands/respond.d.ts.map +1 -1
- package/dist/cli/commands/respond.js +10 -2
- package/dist/cli/commands/send.d.ts.map +1 -1
- package/dist/cli/commands/send.js +20 -11
- package/dist/cli/commands/wait.d.ts +4 -1
- package/dist/cli/commands/wait.d.ts.map +1 -1
- package/dist/cli/commands/wait.js +80 -6
- package/dist/cli/types/index.d.ts +16 -0
- package/dist/cli/types/index.d.ts.map +1 -1
- package/dist/cli/utils/pager.d.ts +28 -0
- package/dist/cli/utils/pager.d.ts.map +1 -0
- package/dist/cli/utils/pager.js +57 -0
- package/dist/lib/detection/ansi.d.ts +30 -0
- package/dist/lib/detection/ansi.d.ts.map +1 -0
- package/dist/lib/detection/ansi.js +39 -0
- package/dist/lib/tmux/transcript-squeeze.d.ts +76 -0
- package/dist/lib/tmux/transcript-squeeze.d.ts.map +1 -0
- package/dist/lib/tmux/transcript-squeeze.js +96 -0
- package/dist/server/server.js +18 -0
- package/dist/server/src/config/tmux-pane-config.js +33 -1
- package/dist/server/src/lib/cli-tools/antigravity.js +2 -1
- package/dist/server/src/lib/cli-tools/codex.js +3 -2
- package/dist/server/src/lib/cli-tools/copilot.js +2 -2
- package/dist/server/src/lib/cli-tools/gemini.js +2 -2
- package/dist/server/src/lib/cli-tools/opencode.js +2 -2
- package/dist/server/src/lib/cli-tools/vibe-local.js +2 -2
- package/dist/server/src/lib/db/agent-instances-db.js +53 -0
- package/dist/server/src/lib/db/migrations/index.js +2 -0
- package/dist/server/src/lib/db/migrations/runner.js +1 -1
- package/dist/server/src/lib/db/migrations/v52-orphaned-worktree-children.js +76 -0
- package/dist/server/src/lib/db/migrations/worktree-child-tables.js +119 -0
- package/dist/server/src/lib/db/worktree-db.js +65 -42
- package/dist/server/src/lib/detection/cli-patterns.js +24 -1
- package/dist/server/src/lib/detection/status-detector.js +52 -2
- package/dist/server/src/lib/session/claude-session.js +4 -2
- package/dist/server/src/lib/tmux/read-mode-pager.js +161 -0
- package/dist/server/src/lib/tmux/read-mode.js +327 -0
- package/dist/server/src/lib/tmux/tmux.js +43 -5
- package/package.json +1 -1
- /package/.next/static/{-BSmW0Ae2YsxyCl_ue7_d → -E7I667pJDTBamyG8M_Z-}/_buildManifest.js +0 -0
- /package/.next/static/{-BSmW0Ae2YsxyCl_ue7_d → -E7I667pJDTBamyG8M_Z-}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* The shell pager that `prefix+g` opens inside a tmux popup (Issue #1623, 案A).
|
|
4
|
+
*
|
|
5
|
+
* ## Why the script is embedded here instead of shipped as a file
|
|
6
|
+
*
|
|
7
|
+
* `package.json`'s `files` list publishes `bin/`, `dist/`, `.next/`, `public/`,
|
|
8
|
+
* `scripts/hooks/` and `.env.example` — NOT `scripts/`. A path into the repo
|
|
9
|
+
* therefore does not exist in a global install, and an `npx` install lives in a
|
|
10
|
+
* cache directory npm may garbage-collect out from under a long-lived `bind-key`.
|
|
11
|
+
* Embedding the text and materializing it into `~/.commandmate/bin/` gives the
|
|
12
|
+
* binding one absolute path that is stable across global / local / npx installs.
|
|
13
|
+
*
|
|
14
|
+
* ## Why the squeeze is reimplemented in awk
|
|
15
|
+
*
|
|
16
|
+
* The popup must not depend on the CommandMate server, a node binary, or a built
|
|
17
|
+
* `dist/` — it only has tmux and a shell. awk is present everywhere those are.
|
|
18
|
+
* The duplication is not left to trust: `tests/unit/lib/tmux/transcript-squeeze.test.ts`
|
|
19
|
+
* runs THIS awk program over real captures and asserts byte equality with
|
|
20
|
+
* `squeezeTranscript()`, so the two cannot drift apart silently.
|
|
21
|
+
*/
|
|
22
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
23
|
+
exports.PAGER_SCRIPT = exports.PAGER_SCRIPT_FILENAME = exports.SQUEEZE_AWK_PROGRAM = exports.SQUEEZE_AWK_LOCALE = void 0;
|
|
24
|
+
/**
|
|
25
|
+
* Locale the squeeze awk program MUST run under.
|
|
26
|
+
*
|
|
27
|
+
* Not cosmetic — the program is byte-oriented by construction, and `%c` is the
|
|
28
|
+
* one place where awk implementations disagree about what a "character" is:
|
|
29
|
+
*
|
|
30
|
+
* | awk | `sprintf("%c", 194)` |
|
|
31
|
+
* |-------------------------|----------------------|
|
|
32
|
+
* | mawk, BWK awk | byte `0xC2` |
|
|
33
|
+
* | gawk, `LC_ALL=C` | byte `0xC2` |
|
|
34
|
+
* | gawk, `*.UTF-8` locale | U+00C2 -> `C3 82` |
|
|
35
|
+
*
|
|
36
|
+
* `NBSP` below is assembled from bytes `C2 A0`, so under gawk in a UTF-8 locale
|
|
37
|
+
* it becomes a four-byte string that can never match a real NBSP — NBSP-only
|
|
38
|
+
* rows stop counting as blank and the frame comes back unsqueezed. That is not
|
|
39
|
+
* hypothetical: it is exactly how this shipped broken, green on macOS (BWK awk)
|
|
40
|
+
* and red on `ubuntu-latest`, whose `/usr/bin/awk` is gawk (Debian ranks the
|
|
41
|
+
* gawk alternative above mawk) under a UTF-8 locale.
|
|
42
|
+
*
|
|
43
|
+
* Pinning the locale — rather than making the regex match both encodings — is
|
|
44
|
+
* deliberate: an alternation covering the character form
|
|
45
|
+
* (`sprintf("%c%c",194,160) "|" sprintf("%c",160)`) fixes gawk but makes BWK awk
|
|
46
|
+
* abort with `multibyte conversion failure` in a UTF-8 locale. Measured on all
|
|
47
|
+
* three implementations, `LC_ALL=C` is the only setting where every one of them
|
|
48
|
+
* agrees, and it is byte-transparent: the same 1000-row fixture comes out with
|
|
49
|
+
* an identical sha256 from BWK awk, mawk and gawk.
|
|
50
|
+
*
|
|
51
|
+
* Scoped to the awk process alone, so `less` and tmux keep the user's locale and
|
|
52
|
+
* still render UTF-8 box drawing.
|
|
53
|
+
*/
|
|
54
|
+
exports.SQUEEZE_AWK_LOCALE = 'C';
|
|
55
|
+
/**
|
|
56
|
+
* awk implementation of {@link import('./transcript-squeeze').squeezeTranscript}.
|
|
57
|
+
*
|
|
58
|
+
* Mirrors the TypeScript rules exactly:
|
|
59
|
+
* - `ANSI` is built from `sprintf("%c")` rather than a regex literal because
|
|
60
|
+
* `\033` / `\a` escapes inside a regex literal are not portable across awk
|
|
61
|
+
* implementations (macOS ships BWK awk, Linux usually gawk/mawk).
|
|
62
|
+
* - NBSP (UTF-8 `C2 A0`) is folded to a space before trimming, because
|
|
63
|
+
* JavaScript's `.trim()` treats U+00A0 as whitespace and awk's `[ \t]` does not.
|
|
64
|
+
* Without this the two implementations disagree on NBSP-only rows. This is the
|
|
65
|
+
* line that requires {@link SQUEEZE_AWK_LOCALE} — read that doc before editing.
|
|
66
|
+
* - Leading/trailing blank runs are dropped, runs of 1-2 kept verbatim, runs of
|
|
67
|
+
* 3+ collapsed to a single row carrying the run's ANSI sequences.
|
|
68
|
+
*/
|
|
69
|
+
exports.SQUEEZE_AWK_PROGRAM = `BEGIN {
|
|
70
|
+
ESC = sprintf("%c", 27); BEL = sprintf("%c", 7); NBSP = sprintf("%c%c", 194, 160)
|
|
71
|
+
ANSI = ESC "\\\\[[0-9;]*[a-zA-Z]|" ESC "\\\\][^" BEL "]*" BEL "|\\\\[[0-9;]*m"
|
|
72
|
+
n = 0
|
|
73
|
+
}
|
|
74
|
+
{ line[++n] = $0 }
|
|
75
|
+
END {
|
|
76
|
+
for (i = 1; i <= n; i++) {
|
|
77
|
+
s = line[i]; gsub(ANSI, "", s); gsub(NBSP, " ", s)
|
|
78
|
+
gsub(/^[ \\t\\r\\f\\v]+|[ \\t\\r\\f\\v]+$/, "", s)
|
|
79
|
+
blank[i] = (s == "")
|
|
80
|
+
}
|
|
81
|
+
seen = 0; i = 1
|
|
82
|
+
while (i <= n) {
|
|
83
|
+
if (!blank[i]) { print line[i]; seen = 1; i++; continue }
|
|
84
|
+
j = i; while (j <= n && blank[j]) j++
|
|
85
|
+
if (seen && j <= n) {
|
|
86
|
+
if (j - i <= 2) { for (k = i; k < j; k++) print line[k] }
|
|
87
|
+
else {
|
|
88
|
+
joined = ""; for (k = i; k < j; k++) joined = joined line[k]
|
|
89
|
+
keep = ""
|
|
90
|
+
while (match(joined, ANSI)) {
|
|
91
|
+
keep = keep substr(joined, RSTART, RLENGTH); joined = substr(joined, RSTART + RLENGTH)
|
|
92
|
+
}
|
|
93
|
+
print keep
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
i = j
|
|
97
|
+
}
|
|
98
|
+
}`;
|
|
99
|
+
/** Basename of the materialized script. */
|
|
100
|
+
exports.PAGER_SCRIPT_FILENAME = 'cm-read-pane.sh';
|
|
101
|
+
/**
|
|
102
|
+
* Full text of the pager script.
|
|
103
|
+
*
|
|
104
|
+
* Session resolution is done INSIDE the script on purpose. `display-popup` does
|
|
105
|
+
* NOT expand tmux formats in its shell-command (measured on 3.5a: a literal
|
|
106
|
+
* `#{session_name}` reaches the shell unexpanded), so the session cannot be baked
|
|
107
|
+
* into the binding. `tmux display-message -p` inside the popup returns the correct
|
|
108
|
+
* session and client, which is what makes one global binding serve every session.
|
|
109
|
+
*
|
|
110
|
+
* `=NAME:` is the exact-match target form required by Issue #1156 — a bare name
|
|
111
|
+
* prefix-matches and would show the `-2` instance's pane while the primary is
|
|
112
|
+
* stopped.
|
|
113
|
+
*/
|
|
114
|
+
exports.PAGER_SCRIPT = `#!/bin/sh
|
|
115
|
+
# CommandMate reading mode (Issue #1623) — generated file, edits are overwritten.
|
|
116
|
+
#
|
|
117
|
+
# Renders the transcript of a CommandMate tmux session with layout-only blank rows
|
|
118
|
+
# squeezed away, so a pager lands on the composer instead of hundreds of rows of
|
|
119
|
+
# padding. Invoked by \`prefix+g\` through \`display-popup -E\`; also runnable by hand:
|
|
120
|
+
#
|
|
121
|
+
# sh ~/.commandmate/bin/${exports.PAGER_SCRIPT_FILENAME} mcbd-claude-my-worktree
|
|
122
|
+
#
|
|
123
|
+
# The popup is a SNAPSHOT. Press the key again to refresh it.
|
|
124
|
+
set -u
|
|
125
|
+
|
|
126
|
+
SESSION="\${1:-}"
|
|
127
|
+
if [ -z "$SESSION" ]; then
|
|
128
|
+
SESSION=$(tmux display-message -p '#{session_name}' 2>/dev/null) || SESSION=""
|
|
129
|
+
fi
|
|
130
|
+
if [ -z "$SESSION" ]; then
|
|
131
|
+
echo "cm-read-pane: no tmux session given and none could be resolved" >&2
|
|
132
|
+
exit 1
|
|
133
|
+
fi
|
|
134
|
+
|
|
135
|
+
LINES_BACK="\${CM_READ_LINES:-1000}"
|
|
136
|
+
case "$LINES_BACK" in
|
|
137
|
+
''|*[!0-9]*) LINES_BACK=1000 ;;
|
|
138
|
+
esac
|
|
139
|
+
|
|
140
|
+
if [ -n "\${CM_READ_PAGER:-}" ]; then
|
|
141
|
+
PAGER_CMD="$CM_READ_PAGER"
|
|
142
|
+
elif command -v less >/dev/null 2>&1; then
|
|
143
|
+
PAGER_CMD="less -R +G"
|
|
144
|
+
else
|
|
145
|
+
PAGER_CMD="cat"
|
|
146
|
+
fi
|
|
147
|
+
|
|
148
|
+
# LC_ALL is pinned for awk ONLY (not for less, which still renders UTF-8 box
|
|
149
|
+
# drawing in the user's locale). Under gawk in a UTF-8 locale, sprintf("%c",194)
|
|
150
|
+
# yields U+00C2 rather than the byte 0xC2 and the NBSP fold below silently stops
|
|
151
|
+
# matching. See SQUEEZE_AWK_LOCALE in read-mode-pager.ts.
|
|
152
|
+
tmux capture-pane -pe -t "=$SESSION:" -S "-$LINES_BACK" -E - \\
|
|
153
|
+
| LC_ALL=${exports.SQUEEZE_AWK_LOCALE} awk '${exports.SQUEEZE_AWK_PROGRAM}' \\
|
|
154
|
+
| $PAGER_CMD
|
|
155
|
+
|
|
156
|
+
# Without a pager the popup would close before anything could be read.
|
|
157
|
+
if [ "$PAGER_CMD" = "cat" ]; then
|
|
158
|
+
printf '\\n[CommandMate] end of transcript — press Enter to close.' >&2
|
|
159
|
+
read -r _unused || true
|
|
160
|
+
fi
|
|
161
|
+
`;
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Reading mode: `prefix+g` opens a squeezed transcript in a tmux popup (Issue #1623, 案A).
|
|
4
|
+
*
|
|
5
|
+
* Attaching to a CommandMate session shows nothing readable. The pane is pinned to
|
|
6
|
+
* 200x1000 (Issue #1163), the agent draws its composer at the bottom and its
|
|
7
|
+
* transcript at the top, and tmux follows the cursor — which sits at row 997.
|
|
8
|
+
* Measured on a live session: an attached 72-row client sees ZERO transcript rows.
|
|
9
|
+
* This module adds an on-demand way to read, WITHOUT touching the geometry that
|
|
10
|
+
* status detection, Auto-Yes and the response saver depend on.
|
|
11
|
+
*
|
|
12
|
+
* ## This module mutates state CommandMate does not own — read before changing it
|
|
13
|
+
*
|
|
14
|
+
* `bind-key` writes the tmux server's GLOBAL prefix key table. Every session on
|
|
15
|
+
* that server inherits it, including ones CommandMate knows nothing about. Four
|
|
16
|
+
* guards keep that intervention honest, and all four are load-bearing:
|
|
17
|
+
*
|
|
18
|
+
* 1. **Capability probe.** `display-popup` is tmux 3.2+. Measured on 3.5a:
|
|
19
|
+
* `tmux list-commands <unknown>` exits 0 with EMPTY stdout, so the exit code is
|
|
20
|
+
* useless as a probe — the presence of output is the signal. Older tmux gets
|
|
21
|
+
* no binding at all (a genuine no-op), and the CLI viewer is the fallback.
|
|
22
|
+
* 2. **Conflict check.** `list-keys -T prefix <key>` exits 1 with `unknown key`
|
|
23
|
+
* when the key is free and exits 0 printing the binding when it is taken. A key
|
|
24
|
+
* already bound to something that is not ours is left ALONE. The Issue's claim
|
|
25
|
+
* that `prefix+g` is unbound on stock tmux 3.5a is true here, but says nothing
|
|
26
|
+
* about a user's own `.tmux.conf`.
|
|
27
|
+
* 3. **Session-name guard.** The binding fires `if-shell -F '#{m:mcbd-*,...}'`, so
|
|
28
|
+
* pressing the key in a non-CommandMate session does nothing (measured: the
|
|
29
|
+
* format yields 1 for `mcbd-*` sessions and 0 for everything else).
|
|
30
|
+
* 4. **Opt-out that converges.** `CM_READ_MODE=off` does not merely skip
|
|
31
|
+
* installation — it REMOVES a binding a previous run installed. That is the
|
|
32
|
+
* uninstall path, and it is why shutdown does not unbind (see below).
|
|
33
|
+
*
|
|
34
|
+
* ## Why shutdown does not unbind
|
|
35
|
+
*
|
|
36
|
+
* CommandMate supports several servers at once (`commandmate start --issue N`),
|
|
37
|
+
* all sharing one tmux server. Unbinding when one of them stops would rip the key
|
|
38
|
+
* out from under the others, and the popup needs no CommandMate server to work —
|
|
39
|
+
* it only shells out to tmux. Removal is therefore an explicit user action
|
|
40
|
+
* (`CM_READ_MODE=off` + restart), not a shutdown side effect.
|
|
41
|
+
*/
|
|
42
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
43
|
+
exports.DEFAULT_READ_MODE_KEY = exports.MCBD_SESSION_PREFIX = void 0;
|
|
44
|
+
exports.getPagerScriptPath = getPagerScriptPath;
|
|
45
|
+
exports.isReadModeEnabled = isReadModeEnabled;
|
|
46
|
+
exports.resolveReadModeKey = resolveReadModeKey;
|
|
47
|
+
exports.quoteForTmuxCommand = quoteForTmuxCommand;
|
|
48
|
+
exports.buildBindKeyArgs = buildBindKeyArgs;
|
|
49
|
+
exports.supportsDisplayPopup = supportsDisplayPopup;
|
|
50
|
+
exports.readExistingBinding = readExistingBinding;
|
|
51
|
+
exports.materializePagerScript = materializePagerScript;
|
|
52
|
+
exports.buildUnbindKeyArgs = buildUnbindKeyArgs;
|
|
53
|
+
exports.reconcileReadModeBinding = reconcileReadModeBinding;
|
|
54
|
+
exports.initReadMode = initReadMode;
|
|
55
|
+
const child_process_1 = require("child_process");
|
|
56
|
+
const util_1 = require("util");
|
|
57
|
+
const os_1 = require("os");
|
|
58
|
+
const path_1 = require("path");
|
|
59
|
+
const fs_1 = require("fs");
|
|
60
|
+
const read_mode_pager_1 = require("./read-mode-pager");
|
|
61
|
+
const logger_1 = require("../logger");
|
|
62
|
+
const execFileAsync = (0, util_1.promisify)(child_process_1.execFile);
|
|
63
|
+
const logger = (0, logger_1.createLogger)('tmux-read-mode');
|
|
64
|
+
/** tmux calls here are interactive-latency, not long-running. */
|
|
65
|
+
const TMUX_TIMEOUT = 5000;
|
|
66
|
+
/**
|
|
67
|
+
* Session-name prefix every CommandMate tmux session carries.
|
|
68
|
+
*
|
|
69
|
+
* Not imported from `cli-tools/base.ts` because it is a template literal there,
|
|
70
|
+
* not a constant. `tests/unit/lib/tmux/read-mode.test.ts` asserts that a real
|
|
71
|
+
* `getSessionName()` still starts with this, so a rename breaks the test instead
|
|
72
|
+
* of silently turning the guard into a permanent "never fire".
|
|
73
|
+
*/
|
|
74
|
+
exports.MCBD_SESSION_PREFIX = 'mcbd-';
|
|
75
|
+
/** Key bound under the prefix table when `CM_READ_MODE_KEY` is unset. */
|
|
76
|
+
exports.DEFAULT_READ_MODE_KEY = 'g';
|
|
77
|
+
/**
|
|
78
|
+
* Accepted `CM_READ_MODE_KEY` values: a single alphanumeric, an F-key, either
|
|
79
|
+
* optionally with a `C-` / `M-` / `S-` modifier. Deliberately narrow — a typo
|
|
80
|
+
* should be rejected loudly rather than bound to something surprising in the
|
|
81
|
+
* user's global key table.
|
|
82
|
+
*/
|
|
83
|
+
const READ_MODE_KEY_PATTERN = /^(?:[CMS]-)?(?:[A-Za-z0-9]|F(?:[1-9]|1[0-2]))$/;
|
|
84
|
+
/** Popup geometry. Large enough to read, small enough to still see it is a popup. */
|
|
85
|
+
const POPUP_WIDTH = '90%';
|
|
86
|
+
const POPUP_HEIGHT = '85%';
|
|
87
|
+
/**
|
|
88
|
+
* Absolute path of the materialized pager script.
|
|
89
|
+
*
|
|
90
|
+
* Always under `~/.commandmate/`, never the install directory and never the cwd:
|
|
91
|
+
* `bind-key` is server-global and long-lived, so the path it names has to outlive
|
|
92
|
+
* a `npx` cache eviction or a `cd`.
|
|
93
|
+
*/
|
|
94
|
+
function getPagerScriptPath() {
|
|
95
|
+
return (0, path_1.join)((0, os_1.homedir)(), '.commandmate', 'bin', read_mode_pager_1.PAGER_SCRIPT_FILENAME);
|
|
96
|
+
}
|
|
97
|
+
/** True unless the user set `CM_READ_MODE=off` (or `0` / `false`). */
|
|
98
|
+
function isReadModeEnabled() {
|
|
99
|
+
const raw = (process.env.CM_READ_MODE ?? '').trim().toLowerCase();
|
|
100
|
+
return raw !== 'off' && raw !== '0' && raw !== 'false';
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Resolve the configured key, or undefined when it is malformed.
|
|
104
|
+
* @returns The key to bind, or undefined if `CM_READ_MODE_KEY` fails validation
|
|
105
|
+
*/
|
|
106
|
+
function resolveReadModeKey() {
|
|
107
|
+
const raw = (process.env.CM_READ_MODE_KEY ?? '').trim();
|
|
108
|
+
if (raw === '')
|
|
109
|
+
return exports.DEFAULT_READ_MODE_KEY;
|
|
110
|
+
return READ_MODE_KEY_PATTERN.test(raw) ? raw : undefined;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Quote a path for the shell-command tmux stores in a binding.
|
|
114
|
+
*
|
|
115
|
+
* tmux lexes the stored command itself before handing words to `/bin/sh`, so the
|
|
116
|
+
* quoting has to survive that pass. POSIX `'...'` with `'\''` for embedded quotes
|
|
117
|
+
* satisfies both. Returns undefined for a path that cannot be represented (a
|
|
118
|
+
* newline has no single-quoted form tmux will parse), which callers treat as
|
|
119
|
+
* "do not install".
|
|
120
|
+
*/
|
|
121
|
+
function quoteForTmuxCommand(path) {
|
|
122
|
+
if (path.includes('\n') || path.includes('\r'))
|
|
123
|
+
return undefined;
|
|
124
|
+
return `'${path.replace(/'/g, "'\\''")}'`;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* The exact `bind-key` argument vector for a given key and script path.
|
|
128
|
+
* Exported so tests assert the wire form rather than a paraphrase of it.
|
|
129
|
+
*/
|
|
130
|
+
function buildBindKeyArgs(key, quotedScriptPath) {
|
|
131
|
+
return [
|
|
132
|
+
'bind-key',
|
|
133
|
+
'-T',
|
|
134
|
+
'prefix',
|
|
135
|
+
key,
|
|
136
|
+
'if-shell',
|
|
137
|
+
'-F',
|
|
138
|
+
`#{m:${exports.MCBD_SESSION_PREFIX}*,#{session_name}}`,
|
|
139
|
+
`display-popup -w ${POPUP_WIDTH} -h ${POPUP_HEIGHT} -E ${quotedScriptPath}`,
|
|
140
|
+
];
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Whether this tmux understands `display-popup` (3.2+).
|
|
144
|
+
*
|
|
145
|
+
* Probes on OUTPUT, not exit status: measured on 3.5a, `list-commands` exits 0
|
|
146
|
+
* for an unknown command name and simply prints nothing.
|
|
147
|
+
*/
|
|
148
|
+
async function supportsDisplayPopup() {
|
|
149
|
+
try {
|
|
150
|
+
const { stdout } = await execFileAsync('tmux', ['list-commands', 'display-popup'], {
|
|
151
|
+
timeout: TMUX_TIMEOUT,
|
|
152
|
+
});
|
|
153
|
+
return stdout.trim() !== '';
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Current prefix-table binding for `key`, or undefined when the key is free.
|
|
161
|
+
*
|
|
162
|
+
* @param key - tmux key name
|
|
163
|
+
* @returns The `bind-key ...` line tmux reports, or undefined if unbound
|
|
164
|
+
*/
|
|
165
|
+
async function readExistingBinding(key) {
|
|
166
|
+
try {
|
|
167
|
+
const { stdout } = await execFileAsync('tmux', ['list-keys', '-T', 'prefix', key], {
|
|
168
|
+
timeout: TMUX_TIMEOUT,
|
|
169
|
+
});
|
|
170
|
+
const line = stdout.trim();
|
|
171
|
+
return line === '' ? undefined : line;
|
|
172
|
+
}
|
|
173
|
+
catch {
|
|
174
|
+
// Exit 1 + `unknown key: <k>` on stderr is how tmux reports "not bound".
|
|
175
|
+
return undefined;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Write the pager script to {@link getPagerScriptPath}, if its content changed.
|
|
180
|
+
*
|
|
181
|
+
* Idempotent by content comparison so a restart does not churn the file's mtime,
|
|
182
|
+
* and re-executable after a `chmod` accident because the mode is re-applied.
|
|
183
|
+
*
|
|
184
|
+
* @returns The absolute path written
|
|
185
|
+
*/
|
|
186
|
+
function materializePagerScript() {
|
|
187
|
+
const scriptPath = getPagerScriptPath();
|
|
188
|
+
(0, fs_1.mkdirSync)((0, path_1.dirname)(scriptPath), { recursive: true });
|
|
189
|
+
let current;
|
|
190
|
+
if ((0, fs_1.existsSync)(scriptPath)) {
|
|
191
|
+
try {
|
|
192
|
+
current = (0, fs_1.readFileSync)(scriptPath, 'utf-8');
|
|
193
|
+
}
|
|
194
|
+
catch {
|
|
195
|
+
current = undefined;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
if (current !== read_mode_pager_1.PAGER_SCRIPT) {
|
|
199
|
+
(0, fs_1.writeFileSync)(scriptPath, read_mode_pager_1.PAGER_SCRIPT, { mode: 0o755 });
|
|
200
|
+
}
|
|
201
|
+
(0, fs_1.chmodSync)(scriptPath, 0o755);
|
|
202
|
+
return scriptPath;
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* The exact argument vector that removes the binding again.
|
|
206
|
+
*
|
|
207
|
+
* Paired with {@link buildBindKeyArgs} and exported for the same reason: the
|
|
208
|
+
* install and uninstall paths are asserted against these builders rather than
|
|
209
|
+
* against re-typed literals, so a test cannot drift from what actually runs —
|
|
210
|
+
* and `tests/unit/config/tmux-live-test-safety.test.ts` keeps its meaning,
|
|
211
|
+
* because the mutating verbs appear in exactly one place, here in `src/`.
|
|
212
|
+
*/
|
|
213
|
+
function buildUnbindKeyArgs(key) {
|
|
214
|
+
return ['unbind-key', '-T', 'prefix', key];
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Remove the binding, but ONLY when it is still the one we installed.
|
|
218
|
+
*
|
|
219
|
+
* A user who rebound the key after us owns it now; clobbering that would be the
|
|
220
|
+
* same rudeness the conflict check exists to avoid.
|
|
221
|
+
*/
|
|
222
|
+
async function removeOwnBinding(key, scriptPath) {
|
|
223
|
+
const existing = await readExistingBinding(key);
|
|
224
|
+
if (existing === undefined)
|
|
225
|
+
return 'not-installed';
|
|
226
|
+
if (!existing.includes(scriptPath))
|
|
227
|
+
return 'key-conflict';
|
|
228
|
+
await execFileAsync('tmux', buildUnbindKeyArgs(key), { timeout: TMUX_TIMEOUT });
|
|
229
|
+
return 'removed';
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Reconcile the tmux prefix key table with the configured reading-mode state.
|
|
233
|
+
*
|
|
234
|
+
* Safe to call on every server start: installing twice is a no-op, and the
|
|
235
|
+
* disabled path removes what an earlier run installed. Never throws — reading
|
|
236
|
+
* mode is a convenience, and no failure here may keep the server from starting.
|
|
237
|
+
*
|
|
238
|
+
* @returns What was done, for logging and for tests
|
|
239
|
+
*/
|
|
240
|
+
async function reconcileReadModeBinding() {
|
|
241
|
+
const key = resolveReadModeKey();
|
|
242
|
+
const scriptPath = getPagerScriptPath();
|
|
243
|
+
if (key === undefined) {
|
|
244
|
+
return {
|
|
245
|
+
installed: false,
|
|
246
|
+
outcome: 'invalid-key',
|
|
247
|
+
key: process.env.CM_READ_MODE_KEY ?? '',
|
|
248
|
+
scriptPath,
|
|
249
|
+
detail: 'CM_READ_MODE_KEY must be a single alphanumeric or F-key, optionally with a C-/M-/S- modifier',
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
try {
|
|
253
|
+
if (!isReadModeEnabled()) {
|
|
254
|
+
const outcome = await removeOwnBinding(key, scriptPath);
|
|
255
|
+
return { installed: false, outcome, key, scriptPath, detail: 'CM_READ_MODE=off' };
|
|
256
|
+
}
|
|
257
|
+
if (!(await supportsDisplayPopup())) {
|
|
258
|
+
return {
|
|
259
|
+
installed: false,
|
|
260
|
+
outcome: 'unsupported-tmux',
|
|
261
|
+
key,
|
|
262
|
+
scriptPath,
|
|
263
|
+
detail: 'display-popup requires tmux 3.2+; use `commandmate capture <id> --pane` instead',
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
const quoted = quoteForTmuxCommand(scriptPath);
|
|
267
|
+
if (quoted === undefined) {
|
|
268
|
+
return { installed: false, outcome: 'unsafe-script-path', key, scriptPath };
|
|
269
|
+
}
|
|
270
|
+
const existing = await readExistingBinding(key);
|
|
271
|
+
if (existing !== undefined && !existing.includes(scriptPath)) {
|
|
272
|
+
return {
|
|
273
|
+
installed: false,
|
|
274
|
+
outcome: 'key-conflict',
|
|
275
|
+
key,
|
|
276
|
+
scriptPath,
|
|
277
|
+
detail: `prefix+${key} is already bound: ${existing}`,
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
materializePagerScript();
|
|
281
|
+
await execFileAsync('tmux', buildBindKeyArgs(key, quoted), { timeout: TMUX_TIMEOUT });
|
|
282
|
+
return {
|
|
283
|
+
installed: true,
|
|
284
|
+
outcome: existing === undefined ? 'installed' : 'already-installed',
|
|
285
|
+
key,
|
|
286
|
+
scriptPath,
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
catch (error) {
|
|
290
|
+
return {
|
|
291
|
+
installed: false,
|
|
292
|
+
outcome: 'error',
|
|
293
|
+
key,
|
|
294
|
+
scriptPath,
|
|
295
|
+
detail: error instanceof Error ? error.message : String(error),
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* Startup entry point: reconcile and log the outcome once.
|
|
301
|
+
*
|
|
302
|
+
* `server.ts` must reach this through `await import()`, never a top-level static
|
|
303
|
+
* import — pulling module graphs into server.ts's eval-time graph perturbs Next's
|
|
304
|
+
* AsyncLocalStorage bootstrap under `tsx server.ts` and the first request that
|
|
305
|
+
* compiles middleware dies (see the Skill reconciler note in server.ts).
|
|
306
|
+
*/
|
|
307
|
+
async function initReadMode() {
|
|
308
|
+
const status = await reconcileReadModeBinding();
|
|
309
|
+
switch (status.outcome) {
|
|
310
|
+
case 'installed':
|
|
311
|
+
case 'already-installed':
|
|
312
|
+
logger.info('read-mode:ready', { key: status.key, scriptPath: status.scriptPath });
|
|
313
|
+
break;
|
|
314
|
+
case 'removed':
|
|
315
|
+
case 'not-installed':
|
|
316
|
+
case 'disabled':
|
|
317
|
+
logger.info('read-mode:disabled', { key: status.key, outcome: status.outcome });
|
|
318
|
+
break;
|
|
319
|
+
default:
|
|
320
|
+
logger.warn('read-mode:not-installed', {
|
|
321
|
+
outcome: status.outcome,
|
|
322
|
+
key: status.key,
|
|
323
|
+
detail: status.detail,
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
return status;
|
|
327
|
+
}
|
|
@@ -198,7 +198,7 @@ async function listSessions() {
|
|
|
198
198
|
* await createSession({
|
|
199
199
|
* sessionName: 'my-session',
|
|
200
200
|
* workingDirectory: '/path/to/project',
|
|
201
|
-
* historyLimit:
|
|
201
|
+
* historyLimit: TMUX_HISTORY_LIMIT,
|
|
202
202
|
* });
|
|
203
203
|
* ```
|
|
204
204
|
*/
|
|
@@ -212,7 +212,7 @@ async function createSession(sessionNameOrOptions, cwd) {
|
|
|
212
212
|
// Legacy signature
|
|
213
213
|
sessionName = sessionNameOrOptions;
|
|
214
214
|
workingDirectory = cwd;
|
|
215
|
-
historyLimit =
|
|
215
|
+
historyLimit = tmux_pane_config_1.TMUX_HISTORY_LIMIT;
|
|
216
216
|
windowWidth = tmux_pane_config_1.TUI_PANE_WIDTH;
|
|
217
217
|
windowHeight = tmux_pane_config_1.TUI_PANE_HEIGHT;
|
|
218
218
|
}
|
|
@@ -220,7 +220,7 @@ async function createSession(sessionNameOrOptions, cwd) {
|
|
|
220
220
|
// New signature with options
|
|
221
221
|
sessionName = sessionNameOrOptions.sessionName;
|
|
222
222
|
workingDirectory = sessionNameOrOptions.workingDirectory;
|
|
223
|
-
historyLimit = sessionNameOrOptions.historyLimit ||
|
|
223
|
+
historyLimit = sessionNameOrOptions.historyLimit || tmux_pane_config_1.TMUX_HISTORY_LIMIT;
|
|
224
224
|
windowWidth = sessionNameOrOptions.windowWidth || tmux_pane_config_1.TUI_PANE_WIDTH;
|
|
225
225
|
windowHeight = sessionNameOrOptions.windowHeight || tmux_pane_config_1.TUI_PANE_HEIGHT;
|
|
226
226
|
}
|
|
@@ -228,6 +228,40 @@ async function createSession(sessionNameOrOptions, cwd) {
|
|
|
228
228
|
// Create session with explicit window size to avoid 80x24 default
|
|
229
229
|
// This is critical for TUI tools (Copilot, OpenCode) that use alternate screen
|
|
230
230
|
await execFileAsync('tmux', ['new-session', '-d', '-s', sessionName, '-c', workingDirectory, '-x', String(windowWidth), '-y', String(windowHeight)], { timeout: DEFAULT_TIMEOUT });
|
|
231
|
+
// Issue #1624: `history-limit` must be set BEFORE the pane that uses it exists.
|
|
232
|
+
//
|
|
233
|
+
// It is a session option, but a pane sizes its scrollback buffer ONCE, from
|
|
234
|
+
// the value in effect at pane-creation time. `new-session` above already
|
|
235
|
+
// created window 0 and its pane, so the old order (new-session → set-option)
|
|
236
|
+
// left every pane on tmux's built-in 2000 lines while `show-options` happily
|
|
237
|
+
// reported 50000 — the session option was set, and utterly inert. Measured on
|
|
238
|
+
// tmux 3.5a: pane `#{history_limit}` stayed 2000, and a live
|
|
239
|
+
// `mcbd-codex-*` session sat at 1977/2000 lines used, silently dropping the
|
|
240
|
+
// oldest transcript. `respawn-pane -k` does NOT fix it either (the pane reuses
|
|
241
|
+
// its existing buffer); only creating a NEW pane does.
|
|
242
|
+
await execFileAsync('tmux', ['set-option', '-t', exactTarget(sessionName), 'history-limit', String(historyLimit)], { timeout: DEFAULT_TIMEOUT });
|
|
243
|
+
// Rebuild window 0 IN PLACE so its pane is allocated against the option just
|
|
244
|
+
// set. `-k` replaces the existing window at index 0 rather than appending,
|
|
245
|
+
// which keeps `#{window_index}` at 0 and the session at exactly one window —
|
|
246
|
+
// no call site has to care that the window was recreated.
|
|
247
|
+
//
|
|
248
|
+
// `-c` is REQUIRED and not redundant: a bare `new-window` does NOT inherit the
|
|
249
|
+
// session's `-c` directory, it starts in the tmux CLIENT's cwd (verified: a
|
|
250
|
+
// session created with `-c /usr/local` produced a pane in the server process's
|
|
251
|
+
// cwd instead). Omitting it silently launches every agent in the wrong repo.
|
|
252
|
+
//
|
|
253
|
+
// Best-effort, matching the geometry step below: if this fails the session is
|
|
254
|
+
// still usable, just with tmux's default 2000-line scrollback.
|
|
255
|
+
try {
|
|
256
|
+
await execFileAsync('tmux', ['new-window', '-k', '-t', `${exactTarget(sessionName)}0`, '-c', workingDirectory], { timeout: DEFAULT_TIMEOUT });
|
|
257
|
+
}
|
|
258
|
+
catch (error) {
|
|
259
|
+
logger.warn('session-history-limit:window-rebuild-failed', {
|
|
260
|
+
sessionName,
|
|
261
|
+
historyLimit,
|
|
262
|
+
error: error instanceof Error ? error.message : String(error),
|
|
263
|
+
});
|
|
264
|
+
}
|
|
231
265
|
// Issue #1163: Pin the pane to a fixed height so alternate-screen TUIs
|
|
232
266
|
// (Claude/Codex/etc.) keep enough visible rows for capture-pane.
|
|
233
267
|
//
|
|
@@ -239,9 +273,13 @@ async function createSession(sessionNameOrOptions, cwd) {
|
|
|
239
273
|
// tracking (the global option is never touched), and an explicit
|
|
240
274
|
// `resize-window` then locks in the intended geometry. Best-effort: a failure
|
|
241
275
|
// here must not abort session creation (some environments restrict resize).
|
|
276
|
+
//
|
|
277
|
+
// Issue #1624: this MUST stay AFTER the window rebuild above. A window created
|
|
278
|
+
// by `new-window` does not inherit `window-size manual` from the window it
|
|
279
|
+
// replaced (verified: `show-window-options -v window-size` came back empty),
|
|
280
|
+
// so reconciling first would have the setting thrown away and leave the pane
|
|
281
|
+
// tracking the latest client again.
|
|
242
282
|
await reconcileSessionGeometry(sessionName, { windowWidth, windowHeight });
|
|
243
|
-
// Set history limit
|
|
244
|
-
await execFileAsync('tmux', ['set-option', '-t', exactTarget(sessionName), 'history-limit', String(historyLimit)], { timeout: DEFAULT_TIMEOUT });
|
|
245
283
|
}
|
|
246
284
|
catch (error) {
|
|
247
285
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
package/package.json
CHANGED
|
File without changes
|
|
File without changes
|