@uniqbit/mate-core 0.17.0-canary.9 → 0.17.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. package/package.json +11 -5
  2. package/src/cli/commands/artifact/finish/index.ts +0 -9
  3. package/src/cli/commands/artifact/finish/openspec.ts +2 -2
  4. package/src/cli/commands/artifact/finish/registry.ts +1 -1
  5. package/src/cli/commands/artifact/pending/discovery.ts +1 -1
  6. package/src/cli/commands/artifact/pending/index.ts +0 -16
  7. package/src/cli/commands/cap/graphify.ts +1 -1
  8. package/src/cli/commands/cap/index-cmd.ts +1 -1
  9. package/src/cli/commands/cap/tokensave.ts +4 -3
  10. package/src/cli/commands/companion/companion.ts +4 -0
  11. package/src/cli/commands/companion/hub.ts +2 -1
  12. package/src/cli/commands/companion/list.ts +2 -1
  13. package/src/cli/commands/companion/prepare.ts +2 -2
  14. package/src/cli/commands/companion/update.ts +334 -0
  15. package/src/cli/commands/doctor.ts +2 -1
  16. package/src/cli/commands/install.ts +59 -5
  17. package/src/cli/commands/launch/shared.ts +2 -1
  18. package/src/cli/commands/plugin/plugin.ts +4 -0
  19. package/src/cli/commands/plugin/verify.ts +25 -0
  20. package/src/cli/commands/report/highlight.ts +1 -4
  21. package/src/cli/commands/report/renderer.ts +0 -2
  22. package/src/cli/commands/studio/access.ts +201 -0
  23. package/src/cli/commands/studio/index.ts +207 -26
  24. package/src/cli/commands/studio/openspec-cli.ts +1 -1
  25. package/src/cli/commands/studio/selection.ts +127 -11
  26. package/src/cli/commands/studio/server.ts +355 -64
  27. package/src/cli/commands/studio/terminal-assets.ts +50 -0
  28. package/src/cli/commands/studio/terminal-launch.ts +71 -0
  29. package/src/cli/commands/studio/terminal.ts +593 -0
  30. package/src/cli/commands/studio/vault.ts +320 -75
  31. package/src/cli/commands/studio/views/client.ts +90 -9
  32. package/src/cli/commands/studio/views/companion-picker.tsx +9 -5
  33. package/src/cli/commands/studio/views/companion-selector.tsx +11 -7
  34. package/src/cli/commands/studio/views/document.tsx +411 -107
  35. package/src/cli/commands/studio/views/model.ts +14 -1
  36. package/src/cli/commands/studio/views/selection-fields.tsx +14 -0
  37. package/src/cli/commands/studio/views/styles.ts +106 -13
  38. package/src/cli/commands/studio/views/terminal.tsx +426 -0
  39. package/src/cli/commands/studio/views/vault/state.ts +42 -0
  40. package/src/cli/commands/studio/views/workflow/steps.ts +10 -3
  41. package/src/cli/commands/update.ts +11 -8
  42. package/src/cli/companion-link-wizard.tsx +1 -1
  43. package/src/cli/companion-selector.tsx +1 -1
  44. package/src/cli/launch-selector.tsx +1 -1
  45. package/src/cli/main.ts +2 -1
  46. package/src/cli/usage.ts +7 -3
  47. package/src/cli/write-json-stdout.ts +2 -2
  48. package/src/lib/components/spinner.tsx +2 -2
  49. package/src/lib/components/startup-progress.tsx +1 -1
  50. package/src/lib/context-mode-package.ts +1 -1
  51. package/src/lib/opencode-plugin-package.ts +3 -0
  52. package/src/lib/orchestrator/adapters/opencode.ts +53 -13
  53. package/src/lib/orchestrator/companion-hub.ts +3 -3
  54. package/src/lib/orchestrator/companion-registry-store.ts +1 -1
  55. package/src/lib/orchestrator/config-store.ts +2 -2
  56. package/src/lib/orchestrator/editor.ts +4 -12
  57. package/src/lib/orchestrator/engine-guard.ts +9 -1
  58. package/src/lib/orchestrator/projection-entries.ts +1 -1
  59. package/src/lib/orchestrator/projection-record.ts +1 -1
  60. package/src/lib/orchestrator/projection-types.ts +0 -4
  61. package/src/lib/orchestrator/repo-local-registry.ts +0 -1
  62. package/src/lib/orchestrator/root-context.ts +1 -1
  63. package/src/lib/orchestrator/setup-compatibilities.ts +1 -1
  64. package/src/lib/orchestrator/setup-preflight.ts +1 -1
  65. package/src/lib/orchestrator/types.ts +1 -1
  66. package/src/lib/orchestrator/working-repo-projection.ts +1 -1
  67. package/src/lib/orchestrator/workspace-inventory.ts +2 -2
  68. package/src/lib/package-paths.ts +1 -11
  69. package/src/lib/package-root.ts +43 -0
  70. package/src/lib/prebuilt-workspace.ts +1 -1
  71. package/src/lib/public-npm.ts +1 -1
  72. package/src/opencode/companion-hooks.ts +131 -153
  73. package/src/opencode/index.ts +12 -6
  74. package/src/opencode/tui.tsx +46 -49
  75. package/src/playbooks/companion-guidance.ts +1 -28
  76. package/src/runtime/companion-sync.ts +0 -3
  77. package/src/templates/capabilities/openspec-cap/mate-minimal/schema.yaml +2 -1
  78. package/src/templates/providers/opencode/opencode.json +6 -4
  79. package/src/tools/setup/__snapshots__/runtime-surface-golden.test.ts.snap +500 -408
  80. package/src/tools/setup/capabilities/context-mode.ts +45 -25
  81. package/src/tools/setup/capabilities/graphify-shared.ts +3 -3
  82. package/src/tools/setup/capabilities/graphify.ts +2 -2
  83. package/src/tools/setup/capabilities/openspec.ts +2 -7
  84. package/src/tools/setup/capabilities/react-doctor.ts +1 -1
  85. package/src/tools/setup/capabilities/rtk.ts +2 -2
  86. package/src/tools/setup/capabilities/tokensave.ts +98 -12
  87. package/src/tools/setup/context-services.ts +3 -6
  88. package/src/tools/setup/dynamic-plugins/declarations.ts +47 -0
  89. package/src/tools/setup/dynamic-plugins/frozen.ts +215 -0
  90. package/src/tools/setup/dynamic-plugins/hydrate.ts +1 -40
  91. package/src/tools/setup/dynamic-plugins/loader.ts +16 -0
  92. package/src/tools/setup/dynamic-plugins/policy.ts +55 -0
  93. package/src/tools/setup/dynamic-plugins/staging.ts +53 -0
  94. package/src/tools/setup/dynamic-plugins/verify.ts +70 -0
  95. package/src/tools/setup/mate.ts +1 -1
  96. package/src/tools/setup/providers/claude-format.ts +1 -1
  97. package/src/tools/setup/providers/claude.ts +1 -1
  98. package/src/tools/setup/providers/opencode-format.ts +220 -25
  99. package/src/tools/setup/providers/opencode.ts +63 -21
  100. package/src/tools/setup/runtime-documents.ts +9 -11
  101. package/src/tools/setup.ts +0 -4
  102. package/src/cli/commands/studio/views/pairings.tsx +0 -38
  103. package/src/templates/providers/opencode/tui.json +0 -3
@@ -0,0 +1,14 @@
1
+ /** @jsxImportSource hono/jsx */
2
+
3
+ import { type StudioSelection, toFields } from "../selection";
4
+
5
+ /** The hidden fields of a control's GET form: the whole selection it leads to. */
6
+ export function SelectionFields({ selection }: { selection: StudioSelection }) {
7
+ return (
8
+ <>
9
+ {toFields(selection).map(({ name, value }) => (
10
+ <input key={name} type="hidden" name={name} value={value} />
11
+ ))}
12
+ </>
13
+ );
14
+ }
@@ -112,7 +112,6 @@ body {
112
112
  flex-direction: column;
113
113
  gap: 24px;
114
114
  }
115
- .pairings li { padding: 6px 0; font-size: .84rem; }
116
115
  select, button {
117
116
  font: inherit;
118
117
  color: inherit;
@@ -234,9 +233,6 @@ ul.plain li:last-child { border-bottom: 0; }
234
233
  .sidebar-label { color: var(--muted); font-size: .62rem; font-weight: 750; letter-spacing: .1em; text-transform: uppercase; }
235
234
  .sidebar-scope { display: grid; gap: 7px; }
236
235
  .sidebar-scope select { padding: 8px 9px; font-family: var(--mono); font-size: .72rem; }
237
- .sidebar-pairings { color: var(--muted); font-family: var(--mono); font-size: .67rem; line-height: 1.45; }
238
- .sidebar-pairings .plain li { padding: 4px 0; }
239
- .sidebar-pairings .muted { font-size: .67rem; }
240
236
  .sidebar-nav { display: grid; gap: 3px; }
241
237
  .sidebar-nav-list { display: grid; gap: 2px; margin: 0; padding: 0; list-style: none; }
242
238
  .sidebar-nav button { display: flex; align-items: center; justify-content: space-between; width: 100%; padding: 8px 10px; border-color: transparent; border-radius: 7px; background: transparent; font-size: .8rem; }
@@ -360,22 +356,55 @@ ul.plain li:last-child { border-bottom: 0; }
360
356
  .workflow-option small { color: var(--muted); font-size: .65rem; line-height: 1.35; }
361
357
  .workflow-option .runway-step-copy { justify-self: start; }
362
358
 
363
- .vault-layout { display: grid; grid-template-columns: minmax(190px, .34fr) minmax(0, 1fr); gap: 16px; align-items: start; }
364
- .vault-tree-panel, .vault-editor-panel { min-width: 0; }
365
- .vault-tree-panel nav { display: grid; gap: 2px; }
366
- .vault-directory { color: var(--muted); font-family: var(--mono); font-size: .75rem; }
367
- .vault-directory summary { cursor: pointer; padding: 5px 4px; }
368
- .vault-children { display: grid; gap: 2px; padding-left: 12px; }
359
+ .vault-layout { display: grid; grid-template-columns: minmax(220px, 300px) minmax(0, 1fr); gap: 16px; align-items: start; }
360
+ :root[data-vault-tree="hidden"] .vault-layout { grid-template-columns: minmax(0, 1fr); }
361
+ :root[data-vault-tree="hidden"] .vault-tree-panel { display: none; }
362
+ .vault-tree-panel, .vault-main, .vault-editor-panel { min-width: 0; }
363
+ .vault-tree-panel { position: sticky; top: 16px; display: grid; gap: 10px; max-height: calc(100vh - 32px); overflow: auto; padding: 12px; }
364
+ .vault-tree-head { display: flex; align-items: center; justify-content: space-between; font-size: .86rem; }
365
+ .vault-filter { width: 100%; padding: 6px 10px; border: 1px solid var(--border); border-radius: 6px; background: var(--panel-soft); color: var(--text); font-size: .8rem; }
366
+ .vault-filter:focus { outline: 2px solid var(--accent); outline-offset: 1px; }
367
+ .vault-tree { display: grid; gap: 1px; }
368
+ .vault-icon { position: relative; flex: none; box-sizing: border-box; }
369
+ .vault-icon-folder { width: 15px; height: 11px; margin-top: 2px; border-radius: 1px 2px 2px 2px; background: var(--accent); }
370
+ .vault-icon-folder::before { content: ""; position: absolute; top: -2px; left: 0; width: 6px; height: 3px; border-radius: 1px 1px 0 0; background: var(--accent); }
371
+ .vault-icon-file { width: 11px; height: 14px; margin: 0 2px; border: 1.5px solid var(--muted); border-radius: 1px 4px 1px 1px; }
372
+ .vault-chevron { flex: none; box-sizing: border-box; width: 7px; height: 7px; margin: 0 3px; border-right: 1.5px solid var(--muted); border-bottom: 1.5px solid var(--muted); transform: rotate(-45deg); transition: transform .12s ease; }
373
+ .vault-directory[open] > summary .vault-chevron { transform: rotate(45deg); }
374
+ .vault-directory > summary { display: flex; align-items: center; gap: 6px; padding: 4px 6px; border-radius: 6px; list-style: none; color: var(--text); cursor: pointer; font-size: .8rem; }
375
+ .vault-directory > summary::-webkit-details-marker { display: none; }
376
+ .vault-directory > summary:hover, .vault-directory > summary[aria-current="page"] { background: var(--accent-soft); }
377
+ .vault-children { display: grid; gap: 1px; margin-left: 11px; padding-left: 8px; border-left: 1px solid var(--border-soft); }
369
378
  .vault-file { display: contents; }
370
- .vault-file button { width: 100%; overflow: hidden; padding: 6px 8px; border-color: transparent; background: transparent; color: var(--text); font-family: var(--mono); font-size: .72rem; text-overflow: ellipsis; white-space: nowrap; }
371
- .vault-file button:hover, .vault-file button[aria-current="page"] { border-color: var(--border); background: var(--accent-soft); color: var(--accent); }
379
+ .vault-file[hidden], .vault-directory[hidden] { display: none; }
380
+ .vault-file button { display: flex; align-items: center; gap: 6px; width: 100%; overflow: hidden; padding: 4px 6px 4px 24px; border: 0; border-radius: 6px; background: transparent; color: var(--text); font-size: .8rem; text-align: left; white-space: nowrap; }
381
+ .vault-file button span, .vault-directory > summary span { overflow: hidden; text-overflow: ellipsis; }
382
+ .vault-file button:hover { background: var(--accent-soft); }
383
+ .vault-file button[aria-current="page"] { background: var(--accent-soft); color: var(--accent); font-weight: 600; }
384
+ .vault-bar { display: flex; align-items: center; gap: 12px; min-height: 36px; margin-bottom: 12px; }
385
+ .vault-tree-toggle { flex: none; padding: 5px 10px; font-size: .78rem; }
386
+ .vault-breadcrumb ol { display: flex; flex-wrap: wrap; align-items: center; margin: 0; padding: 0; list-style: none; font-size: .95rem; }
387
+ .vault-breadcrumb li { display: flex; align-items: center; }
388
+ .vault-breadcrumb li + li::before { content: "/"; padding: 0 6px; color: var(--muted); }
389
+ .vault-breadcrumb [aria-current="page"] { font-weight: 600; }
390
+ .vault-crumb { display: contents; }
391
+ .vault-crumb button { padding: 0; border: 0; background: transparent; color: var(--accent); font: inherit; cursor: pointer; }
392
+ .vault-crumb button:hover { text-decoration: underline; }
393
+ .vault-listing { padding: 0; overflow: hidden; }
394
+ .vault-listing > p { margin: 0; padding: 12px 16px; }
395
+ .vault-rows { margin: 0; padding: 0; list-style: none; }
396
+ .vault-rows li + li { border-top: 1px solid var(--border-soft); }
397
+ .vault-row { display: contents; }
398
+ .vault-row button { display: flex; align-items: center; gap: 10px; width: 100%; padding: 9px 16px; border: 0; border-radius: 0; background: transparent; color: var(--text); font-size: .86rem; text-align: left; }
399
+ .vault-row button:hover { background: var(--panel-soft); }
400
+ .vault-row button:hover span { color: var(--accent); text-decoration: underline; }
372
401
  .vault-warning { color: var(--warn); }
373
402
  #vault-editor { display: block; width: 100%; min-height: 62vh; resize: vertical; padding: 16px; border: 1px solid var(--border); border-radius: 8px; background: var(--prompt-bg); color: var(--prompt-text); font-family: var(--mono); font-size: .86rem; line-height: 1.6; }
374
403
  #vault-editor:focus { outline: 2px solid var(--accent); outline-offset: 2px; }
375
404
  #vault-editor[readonly] { cursor: default; opacity: .85; }
376
405
  .vault-incoming { display: grid; gap: 8px; margin-top: 16px; padding: 12px; border: 1px solid var(--warn); border-radius: 8px; background: var(--warn-soft); }
377
406
  .vault-incoming pre { max-height: 220px; overflow: auto; margin: 0; white-space: pre-wrap; font-family: var(--mono); font-size: .78rem; }
378
- @media (max-width: 800px) { .vault-layout { grid-template-columns: 1fr; } }
407
+ @media (max-width: 800px) { .vault-layout { grid-template-columns: 1fr; } .vault-tree-panel { position: static; max-height: 50vh; } }
379
408
 
380
409
  @media (max-width: 650px) {
381
410
  .workflow-console-header { grid-template-columns: 1fr auto; }
@@ -403,4 +432,68 @@ ul.plain li:last-child { border-bottom: 0; }
403
432
  .lookup-table tbody tr:hover td { background: transparent; }
404
433
  .lookup-table .bar { width: 100%; max-width: 180px; }
405
434
  }
435
+
436
+ /** Agent terminal sidebar: every rule is scoped so a page without the terminal is unchanged. */
437
+ .terminal-sidebar {
438
+ --terminal-strip: 44px;
439
+ position: sticky;
440
+ top: 0;
441
+ height: 100vh;
442
+ min-width: 0;
443
+ display: flex;
444
+ background: var(--bg-rail);
445
+ border-left: 1px solid var(--border);
446
+ }
447
+ .terminal-resize { position: absolute; top: 0; bottom: 0; left: -4px; width: 8px; cursor: col-resize; touch-action: none; z-index: 2; }
448
+ .terminal-resize:hover { background: var(--accent-soft); }
449
+ .terminal-strip { display: none; flex-direction: column; align-items: center; gap: 12px; width: 100%; padding: 14px 0; }
450
+ .terminal-body { display: flex; flex-direction: column; gap: 10px; flex: 1; min-width: 0; min-height: 0; padding: 16px 14px 14px; }
451
+ .terminal-head { display: grid; gap: 8px; }
452
+ .terminal-title { display: flex; align-items: center; gap: 8px; }
453
+ .terminal-title h2 { flex: 1; margin: 0; font-size: 15px; }
454
+ .terminal-icon-button { display: grid; place-items: center; width: 28px; height: 28px; padding: 0; font-size: 16px; line-height: 1; text-align: center; }
455
+ #terminal-drawer-close { display: none; }
456
+ .terminal-dot { flex: none; width: 8px; height: 8px; border-radius: 50%; background: var(--muted); }
457
+ .terminal-sidebar[data-terminal-state="connected"] .terminal-dot { background: var(--done); }
458
+ .terminal-sidebar[data-terminal-state="reconnecting"] .terminal-dot { background: var(--warn); }
459
+ .terminal-sidebar[data-terminal-state="detached"] .terminal-dot { background: var(--bad); }
460
+ .terminal-target { margin: 0; color: var(--muted); font-size: .8rem; overflow-wrap: anywhere; }
461
+ .terminal-actions { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
462
+ .terminal-actions .button { padding: 6px 9px; font-size: .78rem; }
463
+ .terminal-status { margin: 0; color: var(--muted); font-size: 12px; }
464
+ .terminal-view { flex: 1; min-height: 160px; background: #000; border-radius: 8px; padding: 6px; overflow: hidden; }
465
+ .terminal-sessions-footer { flex: none; display: grid; gap: 6px; max-height: 28vh; overflow-y: auto; }
466
+ .terminal-sessions-title { margin: 0; font-size: 12px; color: var(--muted); }
467
+ .terminal-sessions { list-style: none; margin: 0; padding: 0; display: grid; gap: 6px; }
468
+ .terminal-sessions li { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; font-size: .75rem; }
469
+ .terminal-sessions .button { padding: 4px 8px; font-size: .72rem; }
470
+ .terminal-drawer-open { display: none; }
471
+
472
+ @media (min-width: 1201px) {
473
+ .shell[data-terminal] { grid-template-columns: 260px minmax(0, 1fr) clamp(360px, var(--terminal-width, min(560px, 40vw)), 70vw); }
474
+ :root[data-terminal-collapsed] .shell[data-terminal] { grid-template-columns: 260px minmax(0, 1fr) var(--terminal-strip, 44px); }
475
+ :root[data-terminal-collapsed] .shell[data-terminal] .terminal-body,
476
+ :root[data-terminal-collapsed] .shell[data-terminal] .terminal-resize { display: none; }
477
+ :root[data-terminal-collapsed] .shell[data-terminal] .terminal-strip { display: flex; }
478
+ }
479
+
480
+ /** Below the side-by-side breakpoint the sidebar is an off-canvas drawer; collapse does not apply. */
481
+ @media (max-width: 1200px) {
482
+ .shell[data-terminal] .terminal-sidebar {
483
+ position: fixed;
484
+ top: 0;
485
+ right: 0;
486
+ width: min(92vw, clamp(360px, var(--terminal-width, min(560px, 40vw)), 70vw));
487
+ height: 100vh;
488
+ z-index: 30;
489
+ box-shadow: var(--shadow);
490
+ transform: translateX(100%);
491
+ visibility: hidden;
492
+ transition: transform .2s, visibility .2s;
493
+ }
494
+ .shell[data-terminal] .terminal-sidebar[data-drawer-open] { transform: none; visibility: visible; }
495
+ .shell[data-terminal] #terminal-collapse { display: none; }
496
+ .shell[data-terminal] #terminal-drawer-close { display: grid; }
497
+ .terminal-drawer-open { display: block; position: fixed; right: 18px; bottom: 18px; z-index: 25; box-shadow: var(--shadow); }
498
+ }
406
499
  `;
@@ -0,0 +1,426 @@
1
+ /** @jsxImportSource hono/jsx */
2
+
3
+ import type { TerminalAgent } from "../terminal";
4
+ import type { StudioTerminalPage } from "./model";
5
+
6
+ export const TERMINAL_TAB_KEY = "mate-studio-terminal-tab";
7
+ export const TERMINAL_SESSION_KEY = "mate-studio-terminal-session";
8
+
9
+ const AGENT_LABELS: Record<TerminalAgent, string> = {
10
+ claude: "Claude Code",
11
+ opencode: "OpenCode",
12
+ };
13
+
14
+ export const TERMINAL_WIDTH_KEY = "mate-studio-terminal-width";
15
+ export const TERMINAL_COLLAPSED_KEY = "mate-studio-terminal-collapsed";
16
+ const TERMINAL_MIN_WIDTH = 360;
17
+ const TERMINAL_MAX_WIDTH_RATIO = 0.7;
18
+
19
+ /** Shared by the prepaint and sidebar scripts so both clamp alike. */
20
+ const CLAMP_WIDTH_SOURCE = `function clampWidth(px) {
21
+ return Math.round(Math.max(${TERMINAL_MIN_WIDTH}, Math.min(px, window.innerWidth * ${TERMINAL_MAX_WIDTH_RATIO})));
22
+ }`;
23
+
24
+ /**
25
+ * A direct grid child of `.shell`: sticky positioning breaks under an ancestor
26
+ * that sets `overflow`, and `.main` is not guaranteed to stay free of one.
27
+ */
28
+ export function TerminalSidebar({ terminal }: { terminal: StudioTerminalPage }) {
29
+ const target = terminal.target;
30
+ return (
31
+ <aside
32
+ className="terminal-sidebar"
33
+ id="studio-terminal-panel"
34
+ aria-label="Agent terminal"
35
+ data-terminal-companion={target?.digest}
36
+ data-terminal-state="reconnecting"
37
+ >
38
+ <div
39
+ className="terminal-resize"
40
+ id="terminal-resize"
41
+ role="separator"
42
+ aria-orientation="vertical"
43
+ aria-label="Resize the terminal"
44
+ />
45
+ <div className="terminal-strip">
46
+ <button
47
+ type="button"
48
+ className="terminal-icon-button"
49
+ id="terminal-expand"
50
+ aria-controls="terminal-body"
51
+ aria-expanded="false"
52
+ aria-label="Expand the terminal"
53
+ >
54
+ ‹
55
+ </button>
56
+ <span className="terminal-dot" aria-hidden="true" />
57
+ </div>
58
+ <div className="terminal-body" id="terminal-body">
59
+ <header className="terminal-head">
60
+ <div className="terminal-title">
61
+ <span className="terminal-dot" aria-hidden="true" />
62
+ <h2>Agent terminal</h2>
63
+ <button
64
+ type="button"
65
+ className="terminal-icon-button"
66
+ id="terminal-collapse"
67
+ aria-controls="terminal-body"
68
+ aria-expanded="true"
69
+ aria-label="Collapse the terminal"
70
+ >
71
+ ›
72
+ </button>
73
+ <button
74
+ type="button"
75
+ className="terminal-icon-button"
76
+ id="terminal-drawer-close"
77
+ aria-label="Close the terminal"
78
+ >
79
+ ×
80
+ </button>
81
+ </div>
82
+ <p className="terminal-target">
83
+ {target ? (
84
+ <>
85
+ Launches against <code>{target.path}</code>
86
+ {terminal.pinned ? " (fixed when Studio started)" : null}
87
+ </>
88
+ ) : (
89
+ "Select a registered companion to launch an agent."
90
+ )}
91
+ </p>
92
+ <div className="terminal-actions">
93
+ {target && terminal.agents.length > 0 ? (
94
+ terminal.agents.map((agent) => (
95
+ <button key={agent} type="button" className="button" data-terminal-start={agent}>
96
+ Start {AGENT_LABELS[agent]}
97
+ </button>
98
+ ))
99
+ ) : target ? (
100
+ <span className="empty">No agent this companion allows is installed.</span>
101
+ ) : null}
102
+ <button type="button" className="button" id="terminal-reconnect" hidden>
103
+ Reconnect
104
+ </button>
105
+ </div>
106
+ <p className="terminal-status" id="terminal-status" role="status">
107
+ Connecting…
108
+ </p>
109
+ </header>
110
+ <div className="terminal-view" id="studio-terminal" />
111
+ <footer className="terminal-sessions-footer">
112
+ <h3 className="terminal-sessions-title">Sessions</h3>
113
+ <ul className="terminal-sessions" id="terminal-sessions" />
114
+ </footer>
115
+ </div>
116
+ </aside>
117
+ );
118
+ }
119
+
120
+ /** Outside `.shell`, so it never takes a grid cell; shown only below the side-by-side breakpoint. */
121
+ export function TerminalDrawerButton() {
122
+ return (
123
+ <button
124
+ type="button"
125
+ className="terminal-drawer-open"
126
+ id="terminal-drawer-open"
127
+ aria-controls="studio-terminal-panel"
128
+ aria-expanded="false"
129
+ >
130
+ Terminal
131
+ </button>
132
+ );
133
+ }
134
+
135
+ /**
136
+ * Runs in `<head>` with the terminal only, so a navigation paints the
137
+ * remembered width and collapsed state instead of the defaults first.
138
+ */
139
+ export const STUDIO_TERMINAL_PREPAINT_SCRIPT = `(function () {
140
+ ${CLAMP_WIDTH_SOURCE}
141
+ try {
142
+ var width = parseInt(localStorage.getItem(${JSON.stringify(TERMINAL_WIDTH_KEY)}) || "", 10);
143
+ if (width > 0) document.documentElement.style.setProperty("--terminal-width", clampWidth(width) + "px");
144
+ if (localStorage.getItem(${JSON.stringify(TERMINAL_COLLAPSED_KEY)}) === "true") {
145
+ document.documentElement.setAttribute("data-terminal-collapsed", "");
146
+ }
147
+ } catch (error) {
148
+ /* a blocked web store only costs the preference, never the page */
149
+ }
150
+ })();`;
151
+
152
+ /**
153
+ * The sidebar chrome: collapse, drag-to-resize, and the narrow-window drawer.
154
+ * It never touches xterm.js or the socket; the terminal script refits from
155
+ * its own observer whenever this changes the terminal's size.
156
+ */
157
+ export const STUDIO_TERMINAL_SIDEBAR_SCRIPT = `(function () {
158
+ var root = document.documentElement;
159
+ var panel = document.getElementById("studio-terminal-panel");
160
+ if (!panel) return;
161
+ var collapseButton = document.getElementById("terminal-collapse");
162
+ var expandButton = document.getElementById("terminal-expand");
163
+ var handle = document.getElementById("terminal-resize");
164
+ var opener = document.getElementById("terminal-drawer-open");
165
+ var closer = document.getElementById("terminal-drawer-close");
166
+ ${CLAMP_WIDTH_SOURCE}
167
+ function save(key, value) {
168
+ try { localStorage.setItem(key, value); } catch (error) { /* only the preference is lost */ }
169
+ }
170
+
171
+ function updateCollapseControls(collapsed) {
172
+ if (collapseButton) collapseButton.setAttribute("aria-expanded", String(!collapsed));
173
+ if (expandButton) expandButton.setAttribute("aria-expanded", String(!collapsed));
174
+ }
175
+ function setCollapsed(collapsed) {
176
+ if (collapsed) root.setAttribute("data-terminal-collapsed", "");
177
+ else root.removeAttribute("data-terminal-collapsed");
178
+ updateCollapseControls(collapsed);
179
+ save(${JSON.stringify(TERMINAL_COLLAPSED_KEY)}, String(collapsed));
180
+ }
181
+ updateCollapseControls(root.hasAttribute("data-terminal-collapsed"));
182
+ if (collapseButton) collapseButton.addEventListener("click", function () { setCollapsed(true); });
183
+ if (expandButton) expandButton.addEventListener("click", function () { setCollapsed(false); });
184
+
185
+ if (handle) {
186
+ var dragging = false;
187
+ var width = 0;
188
+ handle.addEventListener("pointerdown", function (event) {
189
+ dragging = true;
190
+ handle.setPointerCapture(event.pointerId);
191
+ event.preventDefault();
192
+ });
193
+ handle.addEventListener("pointermove", function (event) {
194
+ if (!dragging) return;
195
+ width = clampWidth(window.innerWidth - event.clientX);
196
+ root.style.setProperty("--terminal-width", width + "px");
197
+ });
198
+ var stop = function (event) {
199
+ if (!dragging) return;
200
+ dragging = false;
201
+ if (handle.hasPointerCapture && handle.hasPointerCapture(event.pointerId)) handle.releasePointerCapture(event.pointerId);
202
+ if (width > 0) save(${JSON.stringify(TERMINAL_WIDTH_KEY)}, String(width));
203
+ };
204
+ handle.addEventListener("pointerup", stop);
205
+ handle.addEventListener("pointercancel", stop);
206
+ }
207
+
208
+ function setDrawer(open) {
209
+ if (open) panel.setAttribute("data-drawer-open", "");
210
+ else panel.removeAttribute("data-drawer-open");
211
+ if (opener) opener.setAttribute("aria-expanded", String(open));
212
+ }
213
+ if (opener) opener.addEventListener("click", function () {
214
+ setDrawer(true);
215
+ if (closer) closer.focus();
216
+ });
217
+ if (closer) closer.addEventListener("click", function () {
218
+ setDrawer(false);
219
+ if (opener) opener.focus();
220
+ });
221
+ document.addEventListener("keydown", function (event) {
222
+ if (event.key !== "Escape" || !panel.hasAttribute("data-drawer-open")) return;
223
+ setDrawer(false);
224
+ if (opener) opener.focus();
225
+ });
226
+ })();`;
227
+
228
+ /**
229
+ * Drives the xterm.js view over the terminal WebSocket. The tab id lives in
230
+ * `sessionStorage`, so a reload keeps it and a new tab gets its own; a
231
+ * taken-over tab never reconnects on its own.
232
+ */
233
+ export const STUDIO_TERMINAL_SCRIPT = `(function () {
234
+ var panel = document.getElementById("studio-terminal-panel");
235
+ var mount = document.getElementById("studio-terminal");
236
+ if (!panel || !mount || typeof Terminal === "undefined") return;
237
+ var statusNode = document.getElementById("terminal-status");
238
+ var reconnectButton = document.getElementById("terminal-reconnect");
239
+ var list = document.getElementById("terminal-sessions");
240
+ var companion = panel.getAttribute("data-terminal-companion") || "";
241
+ var MAX_ATTEMPTS = 8;
242
+ var FIRST_DELAY = 2000;
243
+
244
+ function store(key, value) {
245
+ try {
246
+ if (value === null) sessionStorage.removeItem(key);
247
+ else sessionStorage.setItem(key, value);
248
+ } catch (error) { /* a blocked store only costs the reattach */ }
249
+ }
250
+ function load(key) {
251
+ try { return sessionStorage.getItem(key); } catch (error) { return null; }
252
+ }
253
+ var tabId = load(${JSON.stringify(TERMINAL_TAB_KEY)});
254
+ if (!tabId) {
255
+ tabId = (window.crypto && crypto.randomUUID) ? crypto.randomUUID() : String(Math.random()).slice(2);
256
+ store(${JSON.stringify(TERMINAL_TAB_KEY)}, tabId);
257
+ }
258
+
259
+ var term = new Terminal({ cursorBlink: true, allowProposedApi: true, fontFamily: getComputedStyle(document.documentElement).getPropertyValue("--mono") || "monospace", fontSize: 13 });
260
+ var fit = new FitAddon.FitAddon();
261
+ term.loadAddon(fit);
262
+ term.loadAddon(new Unicode11Addon.Unicode11Addon());
263
+ term.unicode.activeVersion = "11";
264
+ term.loadAddon(new WebLinksAddon.WebLinksAddon());
265
+ term.loadAddon(new ClipboardAddon.ClipboardAddon());
266
+ var opened = false;
267
+ var frame = 0;
268
+ /**
269
+ * xterm.js measures its cells on open, so a sidebar collapsed at load opens
270
+ * it only once the area is visible. A zero size never fits: it would send a
271
+ * resize the server rejects.
272
+ */
273
+ function fitNow() {
274
+ frame = 0;
275
+ if (!mount.clientWidth || !mount.clientHeight) return;
276
+ if (!opened) { term.open(mount); opened = true; }
277
+ fit.fit();
278
+ }
279
+ function scheduleFit() { if (!frame) frame = requestAnimationFrame(fitNow); }
280
+ new ResizeObserver(scheduleFit).observe(mount);
281
+ fitNow();
282
+
283
+ var ws = null;
284
+ var attempts = 0;
285
+ var takenOver = false;
286
+ var pending = null;
287
+
288
+ function status(text) { if (statusNode) statusNode.textContent = text; }
289
+ function state(name) { panel.setAttribute("data-terminal-state", name); }
290
+ function currentSession() { return load(${JSON.stringify(TERMINAL_SESSION_KEY)}); }
291
+ function size() { return { cols: term.cols, rows: term.rows }; }
292
+ function send(message) {
293
+ if (ws && ws.readyState === 1) { ws.send(JSON.stringify(message)); return true; }
294
+ return false;
295
+ }
296
+ function attach(sessionId) {
297
+ var message = { type: "attach", sessionId: sessionId, tabId: tabId, cols: term.cols, rows: term.rows };
298
+ if (!send(message)) { pending = message; connect(); }
299
+ }
300
+
301
+ function connect() {
302
+ if (ws && ws.readyState <= 1) return;
303
+ takenOver = false;
304
+ if (reconnectButton) reconnectButton.hidden = true;
305
+ ws = new WebSocket((location.protocol === "https:" ? "wss:" : "ws:") + "//" + location.host + "/api/terminal");
306
+ ws.binaryType = "arraybuffer";
307
+ ws.onopen = function () {
308
+ attempts = 0;
309
+ state("connected");
310
+ status("Connected.");
311
+ if (pending) { ws.send(JSON.stringify(pending)); pending = null; }
312
+ else if (currentSession()) attach(currentSession());
313
+ };
314
+ ws.onmessage = function (event) {
315
+ if (typeof event.data !== "string") { term.write(new Uint8Array(event.data)); return; }
316
+ var message;
317
+ try { message = JSON.parse(event.data); } catch (error) { return; }
318
+ if (message.type === "ready") {
319
+ store(${JSON.stringify(TERMINAL_SESSION_KEY)}, message.sessionId);
320
+ term.reset();
321
+ state("connected");
322
+ status("Connected to " + message.agent + " in " + message.companionPath + ".");
323
+ term.focus();
324
+ } else if (message.type === "exit") {
325
+ store(${JSON.stringify(TERMINAL_SESSION_KEY)}, null);
326
+ state("detached");
327
+ status("The agent exited with status " + message.status + ".");
328
+ } else if (message.type === "ended") {
329
+ store(${JSON.stringify(TERMINAL_SESSION_KEY)}, null);
330
+ state("detached");
331
+ status("The session ended" + (message.reason ? ": " + message.reason : "") + ".");
332
+ } else if (message.type === "detached") {
333
+ state("detached");
334
+ status("Detached (" + message.reason + "); the agent keeps running.");
335
+ } else if (message.type === "takenOver") {
336
+ takenOver = true;
337
+ state("detached");
338
+ status("Another tab took over this session.");
339
+ if (reconnectButton) reconnectButton.hidden = false;
340
+ } else if (message.type === "gone") {
341
+ store(${JSON.stringify(TERMINAL_SESSION_KEY)}, null);
342
+ state("detached");
343
+ status("That session no longer exists.");
344
+ } else if (message.type === "error") {
345
+ status(message.reason);
346
+ }
347
+ refreshSessions();
348
+ };
349
+ ws.onclose = function () {
350
+ ws = null;
351
+ if (takenOver) return;
352
+ if (attempts >= MAX_ATTEMPTS) {
353
+ state("detached");
354
+ status("Disconnected.");
355
+ if (reconnectButton) reconnectButton.hidden = false;
356
+ return;
357
+ }
358
+ var delay = FIRST_DELAY * Math.pow(2, attempts);
359
+ attempts += 1;
360
+ state("reconnecting");
361
+ status("Reconnecting (attempt " + attempts + " of " + MAX_ATTEMPTS + ")…");
362
+ setTimeout(connect, delay);
363
+ };
364
+ }
365
+
366
+ term.onData(function (data) { send({ type: "input", data: data }); });
367
+ var sent = { cols: term.cols, rows: term.rows };
368
+ term.onResize(function (next) {
369
+ if (next.cols === sent.cols && next.rows === sent.rows) return;
370
+ sent = { cols: next.cols, rows: next.rows };
371
+ send({ type: "resize", cols: next.cols, rows: next.rows });
372
+ });
373
+
374
+ Array.prototype.forEach.call(document.querySelectorAll("[data-terminal-start]"), function (button) {
375
+ button.addEventListener("click", function () {
376
+ var dims = size();
377
+ var message = { type: "start", agent: button.getAttribute("data-terminal-start"), companion: companion, tabId: tabId, cols: dims.cols, rows: dims.rows };
378
+ if (!send(message)) { pending = message; connect(); }
379
+ });
380
+ });
381
+ if (reconnectButton) reconnectButton.addEventListener("click", function () { attempts = 0; connect(); });
382
+
383
+ function age(seconds) {
384
+ if (seconds < 60) return seconds + " s";
385
+ if (seconds < 3600) return Math.floor(seconds / 60) + " min";
386
+ return Math.floor(seconds / 3600) + " h";
387
+ }
388
+ function button(label, onClick) {
389
+ var node = document.createElement("button");
390
+ node.type = "button";
391
+ node.className = "button";
392
+ node.textContent = label;
393
+ node.addEventListener("click", onClick);
394
+ return node;
395
+ }
396
+ function refreshSessions() {
397
+ if (!list) return;
398
+ fetch("/api/terminal/sessions", { credentials: "same-origin" }).then(function (response) {
399
+ return response.ok ? response.json() : { sessions: [] };
400
+ }).then(function (body) {
401
+ while (list.firstChild) list.removeChild(list.firstChild);
402
+ var mine = currentSession();
403
+ if (!body.sessions.length) {
404
+ var empty = document.createElement("li");
405
+ empty.className = "empty";
406
+ empty.textContent = "No sessions are running.";
407
+ list.appendChild(empty);
408
+ }
409
+ body.sessions.forEach(function (session) {
410
+ var item = document.createElement("li");
411
+ var label = document.createElement("span");
412
+ label.textContent = session.agent + " · " + session.companionPath + " · " + (session.id === mine ? "this tab" : session.attached ? "attached elsewhere" : "detached") + " · " + age(session.ageSeconds);
413
+ item.appendChild(label);
414
+ if (session.id !== mine) item.appendChild(button(session.attached ? "Take over" : "Attach", function () { attach(session.id); }));
415
+ item.appendChild(button("End", function () {
416
+ fetch("/api/terminal/sessions/end", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ id: session.id }) }).then(refreshSessions);
417
+ }));
418
+ list.appendChild(item);
419
+ });
420
+ }).catch(function () { /* the next refresh retries */ });
421
+ }
422
+
423
+ connect();
424
+ refreshSessions();
425
+ setInterval(refreshSessions, 5000);
426
+ })();`;
@@ -0,0 +1,42 @@
1
+ import type { StudioVaultSelection } from "../../selection";
2
+ import type { VaultManager, VaultTreeResult } from "../../vault";
3
+ import type { StudioVaultPage } from "../model";
4
+
5
+ export async function vaultState(
6
+ manager: VaultManager,
7
+ companionPath: string,
8
+ sel: StudioVaultSelection,
9
+ options: { tree: "prefetch" | "await" },
10
+ ): Promise<StudioVaultPage> {
11
+ let tree: VaultTreeResult | null = null;
12
+ let failure: string | null = null;
13
+ try {
14
+ tree =
15
+ options.tree === "prefetch"
16
+ ? await manager.prefetch(companionPath, sel.refresh)
17
+ : await manager.tree(companionPath, sel.refresh);
18
+ } catch (error) {
19
+ failure = error instanceof Error ? error.message : String(error);
20
+ }
21
+
22
+ let open: StudioVaultPage["open"] = null;
23
+ let refusal: string | null = null;
24
+ if (sel.openPath) {
25
+ try {
26
+ open = await manager.open(companionPath, sel.openPath);
27
+ } catch (error) {
28
+ refusal = error instanceof Error ? error.message : String(error);
29
+ }
30
+ }
31
+
32
+ return {
33
+ tree: tree?.tree ?? null,
34
+ open,
35
+ refusal,
36
+ incoming: null,
37
+ overwritten: null,
38
+ watching: tree?.watching ?? false,
39
+ warning: tree?.warning ?? null,
40
+ failure,
41
+ };
42
+ }
@@ -240,13 +240,20 @@ export function workflowPlan(
240
240
  profile: "mate-minimal",
241
241
  label: "mate-minimal",
242
242
  description:
243
- "Short path that creates only specs and tasks for one-Area, low-risk changes; switch to mate-v1 when uncertain.",
243
+ "Short path that explores first, then creates only specs and tasks for one-Area, low-risk changes; switch to mate-v1 when uncertain.",
244
244
  steps: [
245
+ skillStep(
246
+ "explore",
247
+ "explore",
248
+ "Resolves repository evidence, unknowns, options, and direction without adding a planning artifact.",
249
+ "It gives the minimal proposal step an agreed direction while keeping the output to specs and tasks.",
250
+ "mate-minimal",
251
+ ),
245
252
  skillStep(
246
253
  "propose",
247
254
  "propose",
248
- "Creates the concise, parser-compatible behavior change.",
249
- "OpenSpec generates the minimal specs and tasks artifacts from the agreed direction.",
255
+ "Creates the concise, parser-compatible specs and tasks from the explored direction.",
256
+ "OpenSpec turns the agreed direction into only the minimal profile's specs and tasks; proposal and design artifacts are intentionally omitted.",
250
257
  "mate-minimal",
251
258
  ),
252
259
  reviewStep(