@crouton-kit/crouter 0.3.14 → 0.3.16

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.
Files changed (224) hide show
  1. package/dist/build-root.d.ts +8 -0
  2. package/dist/build-root.js +30 -0
  3. package/dist/builtin-personas/design/base.md +3 -7
  4. package/dist/builtin-personas/design/orchestrator.md +4 -3
  5. package/dist/builtin-personas/developer/base.md +3 -7
  6. package/dist/builtin-personas/developer/orchestrator.md +5 -4
  7. package/dist/builtin-personas/explore/base.md +3 -7
  8. package/dist/builtin-personas/explore/orchestrator.md +1 -5
  9. package/dist/builtin-personas/general/base.md +2 -4
  10. package/dist/builtin-personas/general/orchestrator.md +2 -4
  11. package/dist/builtin-personas/lifecycle/resident.md +2 -0
  12. package/dist/builtin-personas/lifecycle/terminal.md +6 -0
  13. package/dist/builtin-personas/orchestration-kernel.md +42 -3
  14. package/dist/builtin-personas/plan/base.md +3 -5
  15. package/dist/builtin-personas/plan/orchestrator.md +5 -4
  16. package/dist/builtin-personas/plan/reviewers/architecture-fit/base.md +9 -0
  17. package/dist/builtin-personas/plan/reviewers/code-smells/base.md +9 -0
  18. package/dist/builtin-personas/plan/reviewers/pattern-consistency/base.md +9 -0
  19. package/dist/builtin-personas/plan/reviewers/requirements-coverage/base.md +9 -0
  20. package/dist/builtin-personas/plan/reviewers/security/base.md +9 -0
  21. package/dist/builtin-personas/review/base.md +3 -5
  22. package/dist/builtin-personas/review/orchestrator.md +2 -6
  23. package/dist/builtin-personas/runtime-base.md +3 -19
  24. package/dist/builtin-personas/spec/base.md +3 -5
  25. package/dist/builtin-personas/spec/orchestrator.md +4 -3
  26. package/dist/builtin-personas/spine/has-manager.md +10 -0
  27. package/dist/builtin-personas/spine/no-manager.md +2 -0
  28. package/dist/builtin-skills/skills/crouter-development/personas/SKILL.md +96 -0
  29. package/dist/builtin-skills/skills/crouter-development/personas/base-prompt/SKILL.md +49 -0
  30. package/dist/builtin-skills/skills/crouter-development/personas/orchestrator-prompt/SKILL.md +49 -0
  31. package/dist/builtin-skills/skills/planning/SKILL.md +1 -1
  32. package/dist/builtin-skills/skills/spec/SKILL.md +2 -2
  33. package/dist/cli.js +6 -29
  34. package/dist/commands/attention.js +76 -7
  35. package/dist/commands/canvas-prune.d.ts +2 -0
  36. package/dist/commands/canvas-prune.js +66 -0
  37. package/dist/commands/canvas.js +5 -8
  38. package/dist/commands/chord.d.ts +2 -0
  39. package/dist/commands/chord.js +143 -0
  40. package/dist/commands/daemon.js +8 -5
  41. package/dist/commands/dashboard.js +2 -0
  42. package/dist/commands/human/prompts.js +28 -27
  43. package/dist/commands/human/queue.js +30 -14
  44. package/dist/commands/human/shared.d.ts +26 -21
  45. package/dist/commands/human/shared.js +45 -67
  46. package/dist/commands/human.js +4 -14
  47. package/dist/commands/node.d.ts +11 -0
  48. package/dist/commands/node.js +221 -99
  49. package/dist/commands/pkg/market-inspect.js +6 -4
  50. package/dist/commands/pkg/market-manage.js +10 -6
  51. package/dist/commands/pkg/market.js +2 -4
  52. package/dist/commands/pkg/plugin-inspect.js +6 -4
  53. package/dist/commands/pkg/plugin-manage.js +12 -7
  54. package/dist/commands/pkg/plugin.js +2 -4
  55. package/dist/commands/pkg.js +0 -4
  56. package/dist/commands/push.js +178 -15
  57. package/dist/commands/revive.js +5 -3
  58. package/dist/commands/skill/author.js +6 -4
  59. package/dist/commands/skill/find.js +8 -5
  60. package/dist/commands/skill/read.js +2 -0
  61. package/dist/commands/skill/state.js +6 -4
  62. package/dist/commands/skill.js +0 -6
  63. package/dist/commands/sys/config.js +21 -7
  64. package/dist/commands/sys/doctor.js +2 -0
  65. package/dist/commands/sys/update.js +4 -0
  66. package/dist/commands/sys.js +0 -6
  67. package/dist/commands/tmux-spread.d.ts +2 -0
  68. package/dist/commands/tmux-spread.js +129 -0
  69. package/dist/core/__tests__/canvas-inbox-watcher.test.js +25 -0
  70. package/dist/core/__tests__/child-followup.test.d.ts +1 -0
  71. package/dist/core/__tests__/child-followup.test.js +83 -0
  72. package/dist/core/__tests__/close.test.d.ts +1 -0
  73. package/dist/core/__tests__/close.test.js +148 -0
  74. package/dist/core/__tests__/context-intro.test.d.ts +1 -0
  75. package/dist/core/__tests__/context-intro.test.js +196 -0
  76. package/dist/core/__tests__/daemon-boot.test.d.ts +1 -0
  77. package/dist/core/__tests__/daemon-boot.test.js +93 -0
  78. package/dist/core/__tests__/daemon-liveness.test.d.ts +1 -0
  79. package/dist/core/__tests__/daemon-liveness.test.js +223 -0
  80. package/dist/core/__tests__/focuses.test.d.ts +1 -0
  81. package/dist/core/__tests__/focuses.test.js +196 -0
  82. package/dist/core/__tests__/fork.test.d.ts +1 -0
  83. package/dist/core/__tests__/fork.test.js +91 -0
  84. package/dist/core/__tests__/home-session.test.d.ts +1 -0
  85. package/dist/core/__tests__/home-session.test.js +153 -0
  86. package/dist/core/__tests__/human-cancel-guard.test.d.ts +1 -0
  87. package/dist/core/__tests__/human-cancel-guard.test.js +49 -0
  88. package/dist/core/__tests__/keystone.test.d.ts +1 -0
  89. package/dist/core/__tests__/keystone.test.js +185 -0
  90. package/dist/core/__tests__/kickoff.test.d.ts +1 -0
  91. package/dist/core/__tests__/kickoff.test.js +89 -0
  92. package/dist/core/__tests__/lifecycle.test.d.ts +1 -0
  93. package/dist/core/__tests__/lifecycle.test.js +178 -0
  94. package/dist/core/__tests__/listing-completeness.test.d.ts +1 -0
  95. package/dist/core/__tests__/listing-completeness.test.js +31 -0
  96. package/dist/core/__tests__/memory.test.d.ts +1 -0
  97. package/dist/core/__tests__/memory.test.js +152 -0
  98. package/dist/core/__tests__/migration.test.d.ts +1 -0
  99. package/dist/core/__tests__/migration.test.js +238 -0
  100. package/dist/core/__tests__/pane-column.test.d.ts +1 -0
  101. package/dist/core/__tests__/pane-column.test.js +153 -0
  102. package/dist/core/__tests__/passive-subscription.test.js +24 -1
  103. package/dist/core/__tests__/persona-compose.test.d.ts +1 -0
  104. package/dist/core/__tests__/persona-compose.test.js +53 -0
  105. package/dist/core/__tests__/persona-subkind.test.d.ts +1 -0
  106. package/dist/core/__tests__/persona-subkind.test.js +62 -0
  107. package/dist/core/__tests__/persona.test.d.ts +1 -0
  108. package/dist/core/__tests__/persona.test.js +107 -0
  109. package/dist/core/__tests__/placement-focus.test.d.ts +1 -0
  110. package/dist/core/__tests__/placement-focus.test.js +266 -0
  111. package/dist/core/__tests__/placement-reconcile.test.d.ts +1 -0
  112. package/dist/core/__tests__/placement-reconcile.test.js +212 -0
  113. package/dist/core/__tests__/placement-revive.test.d.ts +1 -0
  114. package/dist/core/__tests__/placement-revive.test.js +238 -0
  115. package/dist/core/__tests__/placement-teardown.test.d.ts +1 -0
  116. package/dist/core/__tests__/placement-teardown.test.js +178 -0
  117. package/dist/core/__tests__/prune.test.d.ts +1 -0
  118. package/dist/core/__tests__/prune.test.js +116 -0
  119. package/dist/core/__tests__/push-final-guard.test.d.ts +1 -0
  120. package/dist/core/__tests__/push-final-guard.test.js +71 -0
  121. package/dist/core/__tests__/relaunch.test.d.ts +1 -0
  122. package/dist/core/__tests__/relaunch.test.js +334 -0
  123. package/dist/core/__tests__/reset.test.js +26 -7
  124. package/dist/core/__tests__/revive.test.d.ts +1 -0
  125. package/dist/core/__tests__/revive.test.js +217 -0
  126. package/dist/core/__tests__/spawn-root.test.d.ts +1 -0
  127. package/dist/core/__tests__/spawn-root.test.js +73 -0
  128. package/dist/core/__tests__/steer-note.test.d.ts +1 -0
  129. package/dist/core/__tests__/steer-note.test.js +39 -0
  130. package/dist/core/__tests__/stop-guard.test.d.ts +1 -0
  131. package/dist/core/__tests__/stop-guard.test.js +82 -0
  132. package/dist/core/__tests__/subcommand-tier.test.js +35 -33
  133. package/dist/core/__tests__/tmux-surface.test.d.ts +1 -0
  134. package/dist/core/__tests__/tmux-surface.test.js +105 -0
  135. package/dist/core/__tests__/unknown-path.test.js +8 -2
  136. package/dist/core/canvas/attention.d.ts +10 -0
  137. package/dist/core/canvas/attention.js +40 -0
  138. package/dist/core/canvas/canvas.d.ts +66 -7
  139. package/dist/core/canvas/canvas.js +209 -21
  140. package/dist/core/canvas/db.d.ts +8 -0
  141. package/dist/core/canvas/db.js +205 -4
  142. package/dist/core/canvas/focuses.d.ts +22 -0
  143. package/dist/core/canvas/focuses.js +81 -0
  144. package/dist/core/canvas/index.d.ts +3 -0
  145. package/dist/core/canvas/index.js +3 -0
  146. package/dist/core/canvas/labels.d.ts +27 -0
  147. package/dist/core/canvas/labels.js +36 -0
  148. package/dist/core/canvas/render.js +25 -10
  149. package/dist/core/canvas/telemetry.d.ts +14 -0
  150. package/dist/core/canvas/telemetry.js +35 -0
  151. package/dist/core/canvas/types.d.ts +115 -12
  152. package/dist/core/command.d.ts +25 -1
  153. package/dist/core/command.js +23 -15
  154. package/dist/core/config.js +36 -2
  155. package/dist/core/feed/feed.js +3 -3
  156. package/dist/core/feed/inbox.d.ts +3 -1
  157. package/dist/core/feed/inbox.js +45 -5
  158. package/dist/core/feed/passive.js +24 -11
  159. package/dist/core/help.d.ts +26 -13
  160. package/dist/core/help.js +44 -37
  161. package/dist/core/personas/index.d.ts +1 -1
  162. package/dist/core/personas/index.js +1 -1
  163. package/dist/core/personas/loader.d.ts +40 -1
  164. package/dist/core/personas/loader.js +63 -1
  165. package/dist/core/personas/resolve.d.ts +13 -6
  166. package/dist/core/personas/resolve.js +46 -34
  167. package/dist/core/runtime/bearings.d.ts +20 -0
  168. package/dist/core/runtime/bearings.js +92 -0
  169. package/dist/core/runtime/close.d.ts +14 -0
  170. package/dist/core/runtime/close.js +151 -0
  171. package/dist/core/runtime/demote.js +24 -12
  172. package/dist/core/runtime/front-door.js +1 -1
  173. package/dist/core/runtime/kickoff.d.ts +23 -6
  174. package/dist/core/runtime/kickoff.js +92 -36
  175. package/dist/core/runtime/launch.d.ts +26 -12
  176. package/dist/core/runtime/launch.js +78 -19
  177. package/dist/core/runtime/lifecycle.d.ts +13 -0
  178. package/dist/core/runtime/lifecycle.js +86 -0
  179. package/dist/core/runtime/memory.d.ts +43 -0
  180. package/dist/core/runtime/memory.js +165 -0
  181. package/dist/core/runtime/naming.d.ts +22 -0
  182. package/dist/core/runtime/naming.js +166 -0
  183. package/dist/core/runtime/nodes.d.ts +39 -1
  184. package/dist/core/runtime/nodes.js +69 -10
  185. package/dist/core/runtime/persona.d.ts +25 -0
  186. package/dist/core/runtime/persona.js +139 -0
  187. package/dist/core/runtime/placement.d.ts +299 -0
  188. package/dist/core/runtime/placement.js +688 -0
  189. package/dist/core/runtime/promote.d.ts +14 -7
  190. package/dist/core/runtime/promote.js +57 -67
  191. package/dist/core/runtime/reset.d.ts +47 -4
  192. package/dist/core/runtime/reset.js +223 -52
  193. package/dist/core/runtime/revive.d.ts +26 -2
  194. package/dist/core/runtime/revive.js +166 -39
  195. package/dist/core/runtime/spawn.d.ts +20 -5
  196. package/dist/core/runtime/spawn.js +163 -43
  197. package/dist/core/runtime/stop-guard.d.ts +1 -1
  198. package/dist/core/runtime/stop-guard.js +18 -8
  199. package/dist/core/runtime/tmux-chrome.d.ts +1 -0
  200. package/dist/core/runtime/tmux-chrome.js +4 -0
  201. package/dist/core/runtime/tmux.d.ts +113 -20
  202. package/dist/core/runtime/tmux.js +221 -39
  203. package/dist/core/spawn.js +15 -0
  204. package/dist/daemon/crtrd.d.ts +12 -1
  205. package/dist/daemon/crtrd.js +152 -34
  206. package/dist/pi-extensions/__tests__/canvas-stophook-agentend.test.d.ts +1 -0
  207. package/dist/pi-extensions/__tests__/canvas-stophook-agentend.test.js +266 -0
  208. package/dist/pi-extensions/canvas-commands.js +16 -13
  209. package/dist/pi-extensions/canvas-context-intro.d.ts +70 -0
  210. package/dist/pi-extensions/canvas-context-intro.js +164 -0
  211. package/dist/pi-extensions/canvas-goal-capture.d.ts +3 -0
  212. package/dist/pi-extensions/canvas-goal-capture.js +15 -1
  213. package/dist/pi-extensions/canvas-inbox-watcher.js +11 -0
  214. package/dist/pi-extensions/canvas-nav.d.ts +12 -4
  215. package/dist/pi-extensions/canvas-nav.js +594 -262
  216. package/dist/pi-extensions/canvas-resume.d.ts +22 -0
  217. package/dist/pi-extensions/canvas-resume.js +173 -0
  218. package/dist/pi-extensions/canvas-stophook.d.ts +16 -0
  219. package/dist/pi-extensions/canvas-stophook.js +340 -228
  220. package/dist/types.d.ts +28 -0
  221. package/dist/types.js +16 -0
  222. package/package.json +2 -2
  223. package/dist/core/runtime/presence.d.ts +0 -38
  224. package/dist/core/runtime/presence.js +0 -154
package/dist/types.d.ts CHANGED
@@ -53,6 +53,28 @@ export interface AutoUpdateConfig {
53
53
  content: AutoUpdateMode;
54
54
  interval_hours: number;
55
55
  }
56
+ /** One canvas-nav action binding: a `crtr` argv string (templated with
57
+ * {id|self|name|manager|subtree}) plus optional confirm gate + menu label.
58
+ * `run` of the sentinel `__graph__` toggles the in-pi GRAPH modal instead of
59
+ * shelling a command. */
60
+ export interface CanvasBind {
61
+ run: string;
62
+ confirm?: boolean;
63
+ desc?: string;
64
+ }
65
+ /** Canvas-nav config (`canvasNav` in config.json). `prefixBinds` are consumed
66
+ * at the tmux alt+c menu layer; `graphBinds` are consumed in-process by the
67
+ * canvas-nav pi extension while in GRAPH. Built-in GRAPH keys
68
+ * (j/k/h/l/g/G/enter/m/e/x/esc) are reserved — `graphBinds` is strictly
69
+ * additive. */
70
+ export interface CanvasNavConfig {
71
+ /** Fallback pi shortcut for GRAPH toggle when NOT in tmux. Default 'alt+g'. */
72
+ prefixKey?: string;
73
+ /** chord key (after alt+c) → action; tmux-menu layer. */
74
+ prefixBinds: Record<string, CanvasBind>;
75
+ /** extra raw key in GRAPH → action; pi-extension layer (additive only). */
76
+ graphBinds: Record<string, CanvasBind>;
77
+ }
56
78
  export interface ScopeConfig {
57
79
  schema_version: number;
58
80
  marketplaces: Record<string, ConfigMarketplaceEntry>;
@@ -60,6 +82,7 @@ export interface ScopeConfig {
60
82
  skills: Record<string, ConfigSkillEntry>;
61
83
  auto_update: AutoUpdateConfig;
62
84
  max_panes_per_window: number;
85
+ canvasNav: CanvasNavConfig;
63
86
  }
64
87
  export interface ScopeState {
65
88
  marketplaces: Record<string, {
@@ -141,5 +164,10 @@ export declare const AGENTS_DIR = "agents";
141
164
  export declare const SCOPE_SKILL_PLUGIN = "_";
142
165
  export declare const DEFAULT_MAX_PANES_PER_WINDOW = 3;
143
166
  export declare function defaultScopeConfig(): ScopeConfig;
167
+ /** Built-in canvas-nav binds so an absent config still gives the intended UX.
168
+ * prefixBinds: g→GRAPH toggle (sentinel), m→focus manager, e→expand to tmux.
169
+ * 1..9 (focus report N) are generated by the menu layer, not literal entries.
170
+ * graphBinds: empty — built-in GRAPH keys are reserved, custom keys add on. */
171
+ export declare function defaultCanvasNavConfig(): CanvasNavConfig;
144
172
  export declare function skillConfigKey(plugin: string, name: string): string;
145
173
  export declare function defaultScopeState(): ScopeState;
package/dist/types.js CHANGED
@@ -36,6 +36,22 @@ export function defaultScopeConfig() {
36
36
  skills: {},
37
37
  auto_update: { crtr: 'notify', content: 'notify', interval_hours: 24 },
38
38
  max_panes_per_window: DEFAULT_MAX_PANES_PER_WINDOW,
39
+ canvasNav: defaultCanvasNavConfig(),
40
+ };
41
+ }
42
+ /** Built-in canvas-nav binds so an absent config still gives the intended UX.
43
+ * prefixBinds: g→GRAPH toggle (sentinel), m→focus manager, e→expand to tmux.
44
+ * 1..9 (focus report N) are generated by the menu layer, not literal entries.
45
+ * graphBinds: empty — built-in GRAPH keys are reserved, custom keys add on. */
46
+ export function defaultCanvasNavConfig() {
47
+ return {
48
+ prefixKey: 'alt+g',
49
+ prefixBinds: {
50
+ g: { run: '__graph__', desc: 'graph view' },
51
+ m: { run: 'node focus {manager}', desc: 'focus manager' },
52
+ e: { run: 'canvas tmux-spread {self}', desc: 'expand to tmux' },
53
+ },
54
+ graphBinds: {},
39
55
  };
40
56
  }
41
57
  export function skillConfigKey(plugin, name) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crouton-kit/crouter",
3
- "version": "0.3.14",
3
+ "version": "0.3.16",
4
4
  "description": "crtr — fast access to skills, plugins, and marketplaces",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -36,7 +36,7 @@
36
36
  },
37
37
  "license": "MIT",
38
38
  "dependencies": {
39
- "@crouton-kit/humanloop": "^0.3.14",
39
+ "@crouton-kit/humanloop": "^0.3.15",
40
40
  "commander": "^13.0.0"
41
41
  },
42
42
  "devDependencies": {
@@ -1,38 +0,0 @@
1
- import type { NodeMeta } from '../canvas/index.js';
2
- /** Persist `nodeId` as the currently focused node. Best-effort; never throws. */
3
- export declare function setFocus(nodeId: string): void;
4
- /** Read the currently focused node id, or null if the pointer is absent or
5
- * empty (no active focus). Best-effort; never throws. */
6
- export declare function getFocus(): string | null;
7
- /** True when the node's tmux window is alive. A falsy tmux_session/window
8
- * always returns false so callers don't need to null-guard. */
9
- export declare function nodeLive(meta: NodeMeta): boolean;
10
- /** Bring a node's tmux window to the foreground and record it as focused.
11
- *
12
- * Strategy:
13
- * - If the node has no live window (`nodeLive` is false), still write the
14
- * focus pointer — the caller (e.g. revive logic) uses `focused:false` to
15
- * know it needs to open a window first.
16
- * - Otherwise call `switchClient` (lands us in the right session) then
17
- * `selectWindow` (picks the right window within it). Both calls are
18
- * best-effort; the focus pointer is always written regardless.
19
- *
20
- * Returns:
21
- * focused — whether the tmux focus actually succeeded.
22
- * session — the tmux session name if one was attempted, null otherwise. */
23
- export declare function focusNode(nodeId: string): {
24
- focused: boolean;
25
- session: string | null;
26
- };
27
- /** Focus a node IN PLACE: bring its pane into the caller's current pane slot
28
- * (swap-pane) instead of navigating the client to the node's own window. This
29
- * is the default for `crtr node focus` and the nav-chrome spine jump — the
30
- * agent appears where you are.
31
- *
32
- * Falls back to window focus when there is no caller pane (not inside tmux) or
33
- * the target pane can't be resolved. `inPlace` reports which path ran. */
34
- export declare function focusNodeInPlace(nodeId: string, callerPane?: string, callerNodeId?: string): {
35
- focused: boolean;
36
- session: string | null;
37
- inPlace: boolean;
38
- };
@@ -1,154 +0,0 @@
1
- // presence.ts — focus pointer + per-node liveness helpers.
2
- //
3
- // The focus pointer (`<crtrHome>/focus.ptr`) is a plain-text file holding the
4
- // node id that currently "has focus" — meaning the user's terminal is showing
5
- // that node's tmux window. It is written on every explicit `focusNode()` call
6
- // and read by the dashboard / status-line to highlight the active node.
7
- //
8
- // This is intentionally a simple file-based pointer rather than a database
9
- // column: focus is transient UI state, not durable business data. A crash
10
- // leaves a stale pointer that the next focusNode() clobbers — harmless.
11
- //
12
- // focusNode() does two things:
13
- // 1. Ensures the user's terminal lands on the right tmux window by calling
14
- // switchClient (cross-session) then selectWindow (in-session). Both are
15
- // best-effort; we set the pointer regardless so the dashboard stays in sync.
16
- // 2. Persists the node id to focus.ptr so any process can quickly read "what
17
- // is the user looking at?".
18
- import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
19
- import { dirname } from 'node:path';
20
- import { join } from 'node:path';
21
- import { crtrHome, getNode, updateNode } from '../canvas/index.js';
22
- import { selectWindow, switchClient, windowAlive, currentTmux, paneOfWindow, swapPaneInPlace, windowOfPane } from './tmux.js';
23
- // ---------------------------------------------------------------------------
24
- // Focus pointer
25
- // ---------------------------------------------------------------------------
26
- /** Absolute path to the focus pointer file. */
27
- function focusPtrPath() {
28
- return join(crtrHome(), 'focus.ptr');
29
- }
30
- /** Persist `nodeId` as the currently focused node. Best-effort; never throws. */
31
- export function setFocus(nodeId) {
32
- try {
33
- const p = focusPtrPath();
34
- mkdirSync(dirname(p), { recursive: true });
35
- writeFileSync(p, nodeId, 'utf8');
36
- }
37
- catch {
38
- /* focus pointer is best-effort; never surface */
39
- }
40
- }
41
- /** Read the currently focused node id, or null if the pointer is absent or
42
- * empty (no active focus). Best-effort; never throws. */
43
- export function getFocus() {
44
- try {
45
- const raw = readFileSync(focusPtrPath(), 'utf8').trim();
46
- return raw !== '' ? raw : null;
47
- }
48
- catch {
49
- return null;
50
- }
51
- }
52
- // ---------------------------------------------------------------------------
53
- // Liveness
54
- // ---------------------------------------------------------------------------
55
- /** True when the node's tmux window is alive. A falsy tmux_session/window
56
- * always returns false so callers don't need to null-guard. */
57
- export function nodeLive(meta) {
58
- return windowAlive(meta.tmux_session, meta.window);
59
- }
60
- // ---------------------------------------------------------------------------
61
- // Focus
62
- // ---------------------------------------------------------------------------
63
- /** Bring a node's tmux window to the foreground and record it as focused.
64
- *
65
- * Strategy:
66
- * - If the node has no live window (`nodeLive` is false), still write the
67
- * focus pointer — the caller (e.g. revive logic) uses `focused:false` to
68
- * know it needs to open a window first.
69
- * - Otherwise call `switchClient` (lands us in the right session) then
70
- * `selectWindow` (picks the right window within it). Both calls are
71
- * best-effort; the focus pointer is always written regardless.
72
- *
73
- * Returns:
74
- * focused — whether the tmux focus actually succeeded.
75
- * session — the tmux session name if one was attempted, null otherwise. */
76
- export function focusNode(nodeId) {
77
- const meta = getNode(nodeId);
78
- // Always write the pointer so the dashboard reflects intent even when focus
79
- // fails (e.g. we're not currently inside tmux).
80
- setFocus(nodeId);
81
- if (meta === null || !nodeLive(meta)) {
82
- // Node not found or window is gone — caller may need to revive.
83
- return { focused: false, session: meta?.tmux_session ?? null };
84
- }
85
- // Both fields are non-null thanks to nodeLive() returning true.
86
- const session = meta.tmux_session;
87
- const window = meta.window;
88
- // Cross-session hop first, then window selection within the session.
89
- // switchClient may be a no-op when already in the same session but is
90
- // always safe to call — tmux handles it gracefully.
91
- const clientOk = switchClient(session);
92
- const windowOk = selectWindow(session, window);
93
- return { focused: clientOk && windowOk, session };
94
- }
95
- /** Focus a node IN PLACE: bring its pane into the caller's current pane slot
96
- * (swap-pane) instead of navigating the client to the node's own window. This
97
- * is the default for `crtr node focus` and the nav-chrome spine jump — the
98
- * agent appears where you are.
99
- *
100
- * Falls back to window focus when there is no caller pane (not inside tmux) or
101
- * the target pane can't be resolved. `inPlace` reports which path ran. */
102
- export function focusNodeInPlace(nodeId, callerPane, callerNodeId) {
103
- const meta = getNode(nodeId);
104
- // Always write the pointer so the dashboard reflects intent even on failure.
105
- setFocus(nodeId);
106
- if (meta === null || !nodeLive(meta)) {
107
- return { focused: false, session: meta?.tmux_session ?? null, inPlace: false };
108
- }
109
- const session = meta.tmux_session;
110
- const window = meta.window;
111
- const pane = callerPane ?? process.env['TMUX_PANE'] ?? currentTmux()?.pane;
112
- // No caller pane (not in tmux) — best we can do is bring the window forefront.
113
- if (pane === undefined || pane === '') {
114
- const ok = switchClient(session) && selectWindow(session, window);
115
- return { focused: ok, session, inPlace: false };
116
- }
117
- const targetPane = paneOfWindow(session, window);
118
- if (targetPane === null) {
119
- const ok = switchClient(session) && selectWindow(session, window);
120
- return { focused: ok, session, inPlace: false };
121
- }
122
- if (targetPane === pane)
123
- return { focused: true, session, inPlace: true }; // already here
124
- // The window the caller's pane currently sits in — the slot the target's pane
125
- // is about to be swapped INTO.
126
- const callerWindow = windowOfPane(pane);
127
- const ok = swapPaneInPlace(targetPane, pane);
128
- // Keep the canvas window mapping in sync with the physical swap. swap-pane
129
- // exchanges the two PANES between their windows (pane ids are stable, windows
130
- // are slots): after the swap the target's pane occupies the caller's window
131
- // and the caller's pane occupies the target's old window. Without this update
132
- // meta.window goes stale, and a later paneOfWindow(session, meta.window)
133
- // resolves the WRONG pane — the bug that made focusing back to a manager a
134
- // no-op (it kept resolving the pane already in view) and made a focused node's
135
- // exit collapse the visible window instead of its background one.
136
- if (ok && callerWindow !== null && callerWindow !== window) {
137
- try {
138
- updateNode(nodeId, { window: callerWindow });
139
- }
140
- catch { /* best-effort */ }
141
- // The caller is the node running this focus (its pi process owns callerPane).
142
- // Its pane moved to the target's old window, so re-point its window there.
143
- // Prefer an explicit id (the `node cycle` tmux binding runs outside any pi,
144
- // so CRTR_NODE_ID is unset there) and fall back to the env for `node focus`.
145
- const cnid = callerNodeId ?? process.env['CRTR_NODE_ID'];
146
- if (cnid !== undefined && cnid.trim() !== '' && cnid !== nodeId) {
147
- try {
148
- updateNode(cnid, { window });
149
- }
150
- catch { /* best-effort */ }
151
- }
152
- }
153
- return { focused: ok, session, inPlace: true };
154
- }