@uniqbit/mate-core 0.18.1-canary.4 → 0.18.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.
- package/package.json +1 -1
- package/src/cli/commands/report/delivery.ts +50 -0
- package/src/cli/commands/report/index.ts +22 -4
- package/src/cli/commands/studio/access.ts +6 -0
- package/src/cli/commands/studio/actions.ts +87 -0
- package/src/cli/commands/studio/reports.ts +176 -0
- package/src/cli/commands/studio/routes.ts +12 -0
- package/src/cli/commands/studio/selection.ts +2 -1
- package/src/cli/commands/studio/server.ts +276 -4
- package/src/cli/commands/studio/terminal-launch.ts +38 -3
- package/src/cli/commands/studio/terminal.ts +48 -4
- package/src/cli/commands/studio/vault.ts +55 -8
- package/src/cli/commands/studio/views/client.ts +263 -27
- package/src/cli/commands/studio/views/document.tsx +168 -20
- package/src/cli/commands/studio/views/model.ts +2 -0
- package/src/cli/commands/studio/views/reports/index.tsx +49 -0
- package/src/cli/commands/studio/views/specs/index.tsx +65 -9
- package/src/cli/commands/studio/views/styles.ts +26 -6
- package/src/cli/commands/studio/views/terminal.tsx +62 -4
- package/src/cli/commands/studio/views/vault/state.ts +1 -0
- package/src/templates/mate-skills/agents/mate-show-me/SKILL.md +11 -5
- package/src/templates/mate-skills/claude/mate-show-me/SKILL.md +11 -5
- package/src/tools/setup/__snapshots__/runtime-surface-golden.test.ts.snap +12 -12
- package/src/tools/setup/mate.ts +1 -1
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
switchView,
|
|
12
12
|
toHref,
|
|
13
13
|
} from "../selection";
|
|
14
|
+
import { REPORTS_ROUTE, VAULT_CHANGES_ROUTE, VAULT_DIR_ROUTE, VAULT_FILTER_ROUTE } from "../routes";
|
|
14
15
|
import { STUDIO_CLIENT_SCRIPT, STUDIO_PREPAINT_SCRIPT } from "./client";
|
|
15
16
|
import { CompanionPicker } from "./companion-picker";
|
|
16
17
|
import { CompanionSelector } from "./companion-selector";
|
|
@@ -18,6 +19,8 @@ import { Dashboard } from "./dashboard/index";
|
|
|
18
19
|
import { CompanionError } from "./error";
|
|
19
20
|
import { formatCollectedAt, type StudioPage, type StudioVaultPage } from "./model";
|
|
20
21
|
import type { VaultTreeNode } from "../vault";
|
|
22
|
+
import { Reports } from "./reports/index";
|
|
23
|
+
import { specActionOffers } from "../actions";
|
|
21
24
|
import { Specs } from "./specs/index";
|
|
22
25
|
import { SelectionFields } from "./selection-fields";
|
|
23
26
|
import { STUDIO_STYLES } from "./styles";
|
|
@@ -55,6 +58,10 @@ const VIEW_DETAILS = {
|
|
|
55
58
|
eyebrow: "Companion files",
|
|
56
59
|
title: "Vault",
|
|
57
60
|
},
|
|
61
|
+
reports: {
|
|
62
|
+
eyebrow: "Agent output",
|
|
63
|
+
title: "Reports",
|
|
64
|
+
},
|
|
58
65
|
} as const;
|
|
59
66
|
|
|
60
67
|
/**
|
|
@@ -99,6 +106,7 @@ function StudioShell({ page }: { page: StudioPage }) {
|
|
|
99
106
|
className="shell"
|
|
100
107
|
data-companion={page.companion ? companionDigest(page.companion.path) : undefined}
|
|
101
108
|
data-terminal={page.terminal ? "" : undefined}
|
|
109
|
+
{...reportAttributes(page)}
|
|
102
110
|
>
|
|
103
111
|
<Sidebar page={page} />
|
|
104
112
|
<main className="main">
|
|
@@ -109,10 +117,27 @@ function StudioShell({ page }: { page: StudioPage }) {
|
|
|
109
117
|
</div>
|
|
110
118
|
{page.terminal ? <TerminalDrawerButton /> : null}
|
|
111
119
|
<div className="toast" id="studio-toast" data-shown="false" />
|
|
120
|
+
<div className="toast toast-report" id="reports-toast" data-shown="false" role="status">
|
|
121
|
+
<span id="reports-toast-title" />
|
|
122
|
+
<a id="reports-toast-open">Open</a>
|
|
123
|
+
</div>
|
|
112
124
|
</>
|
|
113
125
|
);
|
|
114
126
|
}
|
|
115
127
|
|
|
128
|
+
/** What the page client needs to follow the shown companion's reports; nothing without one. */
|
|
129
|
+
function reportAttributes(page: StudioPage): Record<string, string> {
|
|
130
|
+
if (!page.companion) return {};
|
|
131
|
+
const digest = companionDigest(page.companion.path);
|
|
132
|
+
const query = new URLSearchParams({ companion: digest });
|
|
133
|
+
const events = new URLSearchParams({ companion: digest, scope: "reports" });
|
|
134
|
+
return {
|
|
135
|
+
"data-reports-url": `${REPORTS_ROUTE}?${query}`,
|
|
136
|
+
"data-reports-events-url": `${VAULT_CHANGES_ROUTE}?${events}`,
|
|
137
|
+
"data-reports-view-url": toHref(switchView(page.selection, "reports"), "/"),
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
116
141
|
function Content({ page }: { page: StudioPage }) {
|
|
117
142
|
if (!page.companion) {
|
|
118
143
|
return <CompanionPicker inventory={page.inventory} selection={page.selection} />;
|
|
@@ -121,6 +146,7 @@ function Content({ page }: { page: StudioPage }) {
|
|
|
121
146
|
return <CompanionError companionPath={page.error.companionPath} reason={page.error.reason} />;
|
|
122
147
|
}
|
|
123
148
|
if (page.selection.view === "vault") return <Vault page={page} selection={page.selection} />;
|
|
149
|
+
if (page.selection.view === "reports") return <Reports />;
|
|
124
150
|
if (!page.payload) {
|
|
125
151
|
return (
|
|
126
152
|
<section className="panel">
|
|
@@ -131,7 +157,15 @@ function Content({ page }: { page: StudioPage }) {
|
|
|
131
157
|
if (page.selection.view === "workflow") {
|
|
132
158
|
return <Workflow payload={page.payload} />;
|
|
133
159
|
}
|
|
134
|
-
if (page.selection.view === "specs")
|
|
160
|
+
if (page.selection.view === "specs") {
|
|
161
|
+
const { payload } = page;
|
|
162
|
+
return (
|
|
163
|
+
<Specs
|
|
164
|
+
payload={payload}
|
|
165
|
+
actions={specActionOffers(payload.companionPath, payload.skillInventory, page.terminal)}
|
|
166
|
+
/>
|
|
167
|
+
);
|
|
168
|
+
}
|
|
135
169
|
if (page.selection.view === "skills") return <Skills skills={page.payload.skillInventory} />;
|
|
136
170
|
return <Dashboard payload={page.payload} />;
|
|
137
171
|
}
|
|
@@ -185,6 +219,7 @@ const VIEW_NAV: readonly (readonly [StudioView, string])[] = [
|
|
|
185
219
|
["specs", "Specs"],
|
|
186
220
|
["workflow", "Workflow"],
|
|
187
221
|
["skills", "Skills"],
|
|
222
|
+
["reports", "Reports"],
|
|
188
223
|
];
|
|
189
224
|
|
|
190
225
|
/**
|
|
@@ -201,6 +236,11 @@ function ViewNav({ selection }: { selection: StudioSelection }) {
|
|
|
201
236
|
<SelectionFields selection={switchView(selection, view)} />
|
|
202
237
|
<button type="submit" aria-pressed={selection.view === view}>
|
|
203
238
|
<span>{label}</span>
|
|
239
|
+
{view === "reports" ? (
|
|
240
|
+
<span id="reports-badge" className="nav-badge" hidden>
|
|
241
|
+
new
|
|
242
|
+
</span>
|
|
243
|
+
) : null}
|
|
204
244
|
</button>
|
|
205
245
|
</form>
|
|
206
246
|
))}
|
|
@@ -266,6 +306,83 @@ export function renderVaultView(page: StudioPage): string {
|
|
|
266
306
|
);
|
|
267
307
|
}
|
|
268
308
|
|
|
309
|
+
/** A folder's entries, for a folder the page did not expand; `null` when the listed tree lacks it. */
|
|
310
|
+
export function renderVaultChildren(page: StudioPage): string | null {
|
|
311
|
+
const { selection, vault } = page;
|
|
312
|
+
if (!vault?.tree || selection.view !== "vault" || !selection.openDir) return null;
|
|
313
|
+
const folder = findDirectory(vault.tree, selection.openDir);
|
|
314
|
+
if (!folder) return null;
|
|
315
|
+
const nobody = new Set<string>();
|
|
316
|
+
return String(
|
|
317
|
+
<>
|
|
318
|
+
{(folder.children ?? []).toSorted(byKindThenName).map((child) => (
|
|
319
|
+
<VaultNode key={child.path} node={child} selection={selection} expanded={nobody} />
|
|
320
|
+
))}
|
|
321
|
+
</>,
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/** Most matches a filter shows; the rest are counted, not rendered. */
|
|
326
|
+
export const VAULT_FILTER_LIMIT = 200;
|
|
327
|
+
|
|
328
|
+
function matchingFiles(
|
|
329
|
+
tree: VaultTreeNode[],
|
|
330
|
+
needle: string,
|
|
331
|
+
): { shown: VaultTreeNode[]; total: number } {
|
|
332
|
+
const shown: VaultTreeNode[] = [];
|
|
333
|
+
let total = 0;
|
|
334
|
+
const stack = [...tree].reverse();
|
|
335
|
+
while (stack.length > 0) {
|
|
336
|
+
const node = stack.pop()!;
|
|
337
|
+
if (node.kind === "directory") {
|
|
338
|
+
for (let index = (node.children?.length ?? 0) - 1; index >= 0; index -= 1) {
|
|
339
|
+
stack.push(node.children![index]!);
|
|
340
|
+
}
|
|
341
|
+
} else if (segments(node.path).join("/").toLowerCase().includes(needle)) {
|
|
342
|
+
total += 1;
|
|
343
|
+
if (shown.length < VAULT_FILTER_LIMIT) shown.push(node);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
return { shown, total };
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/** Name-filter results over the whole listed tree, bounded; never touches the disk. */
|
|
350
|
+
export function renderVaultMatches(page: StudioPage, query: string): string {
|
|
351
|
+
const { selection, vault } = page;
|
|
352
|
+
const needle = query.trim().toLowerCase();
|
|
353
|
+
if (!vault?.tree || selection.view !== "vault" || !needle) return "";
|
|
354
|
+
const { shown, total } = matchingFiles(vault.tree, needle);
|
|
355
|
+
return String(
|
|
356
|
+
<>
|
|
357
|
+
{shown.length === 0 ? (
|
|
358
|
+
<p className="empty">No files match.</p>
|
|
359
|
+
) : (
|
|
360
|
+
<nav className="vault-tree vault-results" aria-label="Matching files">
|
|
361
|
+
{shown.map((node) => (
|
|
362
|
+
<VaultFileLink
|
|
363
|
+
key={node.path}
|
|
364
|
+
node={node}
|
|
365
|
+
selection={selection}
|
|
366
|
+
label={segments(node.path).join("/")}
|
|
367
|
+
/>
|
|
368
|
+
))}
|
|
369
|
+
</nav>
|
|
370
|
+
)}
|
|
371
|
+
{total > shown.length ? (
|
|
372
|
+
<p className="note" data-vault-more>
|
|
373
|
+
{total - shown.length} more match{total - shown.length === 1 ? "" : "es"}; refine the
|
|
374
|
+
filter.
|
|
375
|
+
</p>
|
|
376
|
+
) : null}
|
|
377
|
+
</>,
|
|
378
|
+
);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/** The selection a read-only vault route is addressed by: no one-shot refresh. */
|
|
382
|
+
function plain(selection: StudioVaultSelection): StudioVaultSelection {
|
|
383
|
+
return { ...selection, refresh: false };
|
|
384
|
+
}
|
|
385
|
+
|
|
269
386
|
function segments(relative: string): string[] {
|
|
270
387
|
return relative.split(/[\\/]/).filter(Boolean);
|
|
271
388
|
}
|
|
@@ -326,11 +443,19 @@ function Vault({ page, selection }: { page: StudioPage; selection: StudioVaultSe
|
|
|
326
443
|
className="vault-layout"
|
|
327
444
|
data-vault-layout
|
|
328
445
|
data-vault-view={vault.tree === null ? toHref(selection, "/api/vault/view") : undefined}
|
|
446
|
+
data-vault-refresh-url={toHref(refresh(selection), "/api/vault/view")}
|
|
447
|
+
data-vault-filter-url={toHref(plain(selection), VAULT_FILTER_ROUTE)}
|
|
448
|
+
data-vault-changes-url={toHref(plain(selection), VAULT_CHANGES_ROUTE)}
|
|
329
449
|
>
|
|
330
450
|
<aside className="panel vault-tree-panel" id="vault-tree-panel" aria-label="Files">
|
|
331
451
|
<div className="vault-tree-head">
|
|
332
452
|
<strong>Files</strong>
|
|
453
|
+
<RefreshTree selection={selection} />
|
|
333
454
|
</div>
|
|
455
|
+
<p id="vault-stale" className="note vault-warning" role="status" hidden>
|
|
456
|
+
Files changed — refresh tree.
|
|
457
|
+
</p>
|
|
458
|
+
<p id="vault-refresh-status" className="note vault-warning" role="status" hidden />
|
|
334
459
|
<input
|
|
335
460
|
type="search"
|
|
336
461
|
id="vault-filter"
|
|
@@ -339,6 +464,7 @@ function Vault({ page, selection }: { page: StudioPage; selection: StudioVaultSe
|
|
|
339
464
|
aria-label="Go to file"
|
|
340
465
|
hidden
|
|
341
466
|
/>
|
|
467
|
+
<div id="vault-filter-results" data-vault-filter-results hidden />
|
|
342
468
|
<VaultTreeSlot vault={vault} selection={selection} />
|
|
343
469
|
</aside>
|
|
344
470
|
<div className="vault-main">
|
|
@@ -362,9 +488,10 @@ function Vault({ page, selection }: { page: StudioPage; selection: StudioVaultSe
|
|
|
362
488
|
);
|
|
363
489
|
}
|
|
364
490
|
|
|
491
|
+
/** The refresh that keeps the tree and filter data current; without script it is a plain reload. */
|
|
365
492
|
function RefreshTree({ selection }: { selection: StudioVaultSelection }) {
|
|
366
493
|
return (
|
|
367
|
-
<form method="get" action="/" data-studio-navigation>
|
|
494
|
+
<form method="get" action="/" data-studio-navigation data-vault-refresh>
|
|
368
495
|
<SelectionFields selection={refresh(selection)} />
|
|
369
496
|
<button type="submit">Refresh tree</button>
|
|
370
497
|
</form>
|
|
@@ -382,7 +509,6 @@ function VaultTreeSlot({
|
|
|
382
509
|
return (
|
|
383
510
|
<div data-vault-slot="tree">
|
|
384
511
|
<p className="note vault-warning">The files could not be listed: {vault.failure}</p>
|
|
385
|
-
<RefreshTree selection={selection} />
|
|
386
512
|
</div>
|
|
387
513
|
);
|
|
388
514
|
}
|
|
@@ -395,11 +521,12 @@ function VaultTreeSlot({
|
|
|
395
521
|
}
|
|
396
522
|
const expanded = expandedPaths(selection);
|
|
397
523
|
return (
|
|
398
|
-
<div data-vault-slot="tree">
|
|
524
|
+
<div data-vault-slot="tree" data-vault-generation={vault.generation ?? undefined}>
|
|
399
525
|
{vault.warning ? (
|
|
400
526
|
<p className="note vault-warning">{vault.warning}. The tree may be out of date.</p>
|
|
401
|
-
) : null
|
|
402
|
-
|
|
527
|
+
) : vault.watching ? null : (
|
|
528
|
+
<p className="note vault-warning">The tree may be out of date.</p>
|
|
529
|
+
)}
|
|
403
530
|
{vault.tree.length === 0 ? (
|
|
404
531
|
<p className="empty">This companion has no markdown files.</p>
|
|
405
532
|
) : (
|
|
@@ -431,6 +558,9 @@ function VaultNode({
|
|
|
431
558
|
open={open}
|
|
432
559
|
data-vault-dir={key}
|
|
433
560
|
data-vault-expanded={open ? "" : undefined}
|
|
561
|
+
data-vault-children-url={
|
|
562
|
+
open ? undefined : toHref(openFolder(selection, key), VAULT_DIR_ROUTE)
|
|
563
|
+
}
|
|
434
564
|
>
|
|
435
565
|
<summary
|
|
436
566
|
aria-current={
|
|
@@ -442,27 +572,45 @@ function VaultNode({
|
|
|
442
572
|
<span>{node.name}</span>
|
|
443
573
|
</summary>
|
|
444
574
|
<div className="vault-children">
|
|
445
|
-
{
|
|
446
|
-
|
|
447
|
-
|
|
575
|
+
{open
|
|
576
|
+
? (node.children ?? [])
|
|
577
|
+
.toSorted(byKindThenName)
|
|
578
|
+
.map((child) => (
|
|
579
|
+
<VaultNode
|
|
580
|
+
key={child.path}
|
|
581
|
+
node={child}
|
|
582
|
+
selection={selection}
|
|
583
|
+
expanded={expanded}
|
|
584
|
+
/>
|
|
585
|
+
))
|
|
586
|
+
: null}
|
|
448
587
|
</div>
|
|
449
588
|
</details>
|
|
450
589
|
);
|
|
451
590
|
}
|
|
591
|
+
return <VaultFileLink node={node} selection={selection} label={node.name} />;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
/** A plain link: no form, no hidden fields, no handler of its own. */
|
|
595
|
+
function VaultFileLink({
|
|
596
|
+
node,
|
|
597
|
+
selection,
|
|
598
|
+
label,
|
|
599
|
+
}: {
|
|
600
|
+
node: VaultTreeNode;
|
|
601
|
+
selection: StudioVaultSelection;
|
|
602
|
+
label: string;
|
|
603
|
+
}) {
|
|
452
604
|
return (
|
|
453
|
-
<
|
|
454
|
-
method="get"
|
|
455
|
-
action="/"
|
|
605
|
+
<a
|
|
456
606
|
className="vault-file"
|
|
457
|
-
|
|
458
|
-
data-vault-entry={
|
|
607
|
+
href={toHref(openFile(selection, node.path))}
|
|
608
|
+
data-vault-entry={segments(node.path).join("/")}
|
|
609
|
+
aria-current={selection.openPath === node.path ? "page" : undefined}
|
|
459
610
|
>
|
|
460
|
-
<
|
|
461
|
-
<
|
|
462
|
-
|
|
463
|
-
<span>{node.name}</span>
|
|
464
|
-
</button>
|
|
465
|
-
</form>
|
|
611
|
+
<FileIcon />
|
|
612
|
+
<span>{label}</span>
|
|
613
|
+
</a>
|
|
466
614
|
);
|
|
467
615
|
}
|
|
468
616
|
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/** @jsxImportSource hono/jsx */
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The list, frame and empty note are filled by the page client from
|
|
5
|
+
* `GET /api/reports`; titles are written as text, never markup.
|
|
6
|
+
*/
|
|
7
|
+
export function Reports() {
|
|
8
|
+
return (
|
|
9
|
+
<section className="panel reports-panel" data-reports-view>
|
|
10
|
+
<div className="section-header">
|
|
11
|
+
<div>
|
|
12
|
+
<h3>Hosted reports</h3>
|
|
13
|
+
<p className="section-note">Reports published by agent sessions Studio started.</p>
|
|
14
|
+
</div>
|
|
15
|
+
</div>
|
|
16
|
+
<div className="reports-layout">
|
|
17
|
+
<div className="reports-side">
|
|
18
|
+
<ul id="reports-list" className="reports-list" aria-label="Reports" />
|
|
19
|
+
<template id="reports-item-template">
|
|
20
|
+
<li>
|
|
21
|
+
<button type="button" data-report-id="">
|
|
22
|
+
<span data-report-title />
|
|
23
|
+
<span className="report-time" data-report-time />
|
|
24
|
+
</button>
|
|
25
|
+
</li>
|
|
26
|
+
</template>
|
|
27
|
+
<p id="reports-empty" className="empty">
|
|
28
|
+
No hosted reports yet.
|
|
29
|
+
</p>
|
|
30
|
+
</div>
|
|
31
|
+
<div className="reports-main">
|
|
32
|
+
<div className="reports-bar">
|
|
33
|
+
<a id="reports-open" target="_blank" rel="noopener noreferrer" hidden>
|
|
34
|
+
Open in new tab
|
|
35
|
+
</a>
|
|
36
|
+
</div>
|
|
37
|
+
<iframe
|
|
38
|
+
id="reports-frame"
|
|
39
|
+
className="reports-frame"
|
|
40
|
+
title="Hosted report"
|
|
41
|
+
sandbox="allow-scripts allow-modals"
|
|
42
|
+
referrerPolicy="no-referrer"
|
|
43
|
+
hidden
|
|
44
|
+
/>
|
|
45
|
+
</div>
|
|
46
|
+
</div>
|
|
47
|
+
</section>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
/** @jsxImportSource hono/jsx */
|
|
2
2
|
|
|
3
|
+
import { SUBJECT_PATTERN, type SpecActionOffer } from "../../actions";
|
|
3
4
|
import type { StudioCompanionPayload, StudioSpec } from "../../payload";
|
|
4
5
|
import { Warnings } from "../warnings";
|
|
5
6
|
|
|
7
|
+
const NO_ACTIONS: SpecActionOffer[] = [];
|
|
6
8
|
const UNASSIGNED_AREA = "unassigned";
|
|
7
9
|
|
|
8
10
|
/** A spec binds one Area or several, so it appears under each of them. */
|
|
@@ -27,19 +29,70 @@ function specStatus(spec: StudioSpec): { label: string; invalid: boolean } {
|
|
|
27
29
|
|
|
28
30
|
interface SpecsProps {
|
|
29
31
|
payload: StudioCompanionPayload;
|
|
32
|
+
/** Skill actions offered on each spec row; none by default. */
|
|
33
|
+
actions?: SpecActionOffer[];
|
|
30
34
|
}
|
|
31
35
|
|
|
32
36
|
/** Its own view: an Area map is read on its own, not alongside the changes. */
|
|
33
|
-
export function Specs({ payload }: SpecsProps) {
|
|
37
|
+
export function Specs({ payload, actions = NO_ACTIONS }: SpecsProps) {
|
|
34
38
|
return (
|
|
35
39
|
<>
|
|
36
|
-
<SpecsByArea specs={payload.specs} />
|
|
40
|
+
<SpecsByArea specs={payload.specs} actions={actions} />
|
|
37
41
|
<Warnings warnings={payload.warnings} />
|
|
38
42
|
</>
|
|
39
43
|
);
|
|
40
44
|
}
|
|
41
45
|
|
|
42
|
-
function
|
|
46
|
+
function SpecActions({ capability, actions }: { capability: string; actions: SpecActionOffer[] }) {
|
|
47
|
+
if (!SUBJECT_PATTERN.test(capability)) return null;
|
|
48
|
+
return (
|
|
49
|
+
<span className="spec-actions">
|
|
50
|
+
{actions.map(({ action, runAgents, copyAgents }) => (
|
|
51
|
+
<>
|
|
52
|
+
{runAgents.length > 0 ? (
|
|
53
|
+
<button
|
|
54
|
+
type="button"
|
|
55
|
+
className="button spec-action"
|
|
56
|
+
data-studio-action={action.id}
|
|
57
|
+
data-studio-subject={capability}
|
|
58
|
+
data-studio-agents={runAgents.join(" ")}
|
|
59
|
+
>
|
|
60
|
+
{action.label}
|
|
61
|
+
</button>
|
|
62
|
+
) : null}
|
|
63
|
+
<button
|
|
64
|
+
type="button"
|
|
65
|
+
className="runway-step-copy spec-copy"
|
|
66
|
+
aria-label="Copy prompt"
|
|
67
|
+
title="Copy prompt"
|
|
68
|
+
data-studio-copy-prompt={action.id}
|
|
69
|
+
data-prompt-claude={
|
|
70
|
+
copyAgents.includes("claude") ? action.prompts.claude(capability) : undefined
|
|
71
|
+
}
|
|
72
|
+
data-prompt-opencode={
|
|
73
|
+
copyAgents.includes("opencode") ? action.prompts.opencode(capability) : undefined
|
|
74
|
+
}
|
|
75
|
+
>
|
|
76
|
+
<svg
|
|
77
|
+
viewBox="0 0 16 16"
|
|
78
|
+
width="14"
|
|
79
|
+
height="14"
|
|
80
|
+
fill="none"
|
|
81
|
+
stroke="currentColor"
|
|
82
|
+
stroke-width="1.5"
|
|
83
|
+
aria-hidden="true"
|
|
84
|
+
>
|
|
85
|
+
<rect x="5.5" y="5.5" width="8" height="8" rx="1.5" />
|
|
86
|
+
<path d="M10.5 3.5v-.5a1.5 1.5 0 0 0-1.5-1.5H3.5A1.5 1.5 0 0 0 2 3v5.5A1.5 1.5 0 0 0 3.5 10H4" />
|
|
87
|
+
</svg>
|
|
88
|
+
</button>
|
|
89
|
+
</>
|
|
90
|
+
))}
|
|
91
|
+
</span>
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function SpecsByArea({ specs, actions }: { specs: StudioSpec[]; actions: SpecActionOffer[] }) {
|
|
43
96
|
const groups = groupSpecsByArea(specs);
|
|
44
97
|
|
|
45
98
|
return (
|
|
@@ -81,12 +134,15 @@ function SpecsByArea({ specs }: { specs: StudioSpec[] }) {
|
|
|
81
134
|
</span>
|
|
82
135
|
</div>
|
|
83
136
|
</div>
|
|
84
|
-
<span
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
137
|
+
<span className="spec-card-end">
|
|
138
|
+
<SpecActions capability={spec.capability} actions={actions} />
|
|
139
|
+
<span
|
|
140
|
+
className={
|
|
141
|
+
status.invalid ? "spec-status spec-status-invalid" : "spec-status"
|
|
142
|
+
}
|
|
143
|
+
>
|
|
144
|
+
{status.label}
|
|
145
|
+
</span>
|
|
90
146
|
</span>
|
|
91
147
|
</div>
|
|
92
148
|
);
|
|
@@ -201,6 +201,17 @@ ul.plain li:last-child { border-bottom: 0; }
|
|
|
201
201
|
opacity: 0; transition: opacity .16s; pointer-events: none; z-index: 20;
|
|
202
202
|
}
|
|
203
203
|
.toast[data-shown="true"] { opacity: 1; }
|
|
204
|
+
.toast-report { display: flex; gap: 10px; align-items: center; bottom: 64px; }
|
|
205
|
+
.toast-report[data-shown="true"] { pointer-events: auto; }
|
|
206
|
+
.toast-report a { color: inherit; font-weight: 700; text-decoration: underline; }
|
|
207
|
+
.nav-badge { padding: 1px 6px; border-radius: 999px; background: var(--accent); color: #0e1116; font-size: .6rem; font-weight: 750; text-transform: uppercase; }
|
|
208
|
+
.reports-layout { display: grid; grid-template-columns: minmax(200px, 280px) 1fr; gap: 12px; min-height: 60vh; }
|
|
209
|
+
.reports-list { display: grid; gap: 4px; margin: 0; padding: 0; list-style: none; }
|
|
210
|
+
.reports-list button { width: 100%; padding: 8px 10px; text-align: left; font-size: .78rem; }
|
|
211
|
+
.reports-list button[aria-pressed="true"] { border-color: var(--accent); background: var(--accent-soft); }
|
|
212
|
+
.reports-list .report-time { display: block; color: var(--muted); font-size: .66rem; }
|
|
213
|
+
.reports-main { display: grid; grid-template-rows: auto 1fr; gap: 8px; }
|
|
214
|
+
.reports-frame { width: 100%; height: 100%; min-height: 60vh; border: 1px solid var(--border); border-radius: 8px; background: #fff; }
|
|
204
215
|
|
|
205
216
|
.warnings { border-color: var(--warn); }
|
|
206
217
|
.error { border-color: var(--bad); }
|
|
@@ -276,6 +287,11 @@ ul.plain li:last-child { border-bottom: 0; }
|
|
|
276
287
|
.spec-card-row:first-child { padding-top: 0; }
|
|
277
288
|
.spec-card-row strong { overflow: hidden; font-family: var(--mono); font-size: .78rem; text-overflow: ellipsis; white-space: nowrap; }
|
|
278
289
|
.spec-card-meta { display: flex; align-items: center; gap: 7px; color: var(--muted); font-size: .7rem; }
|
|
290
|
+
.spec-card-end { display: inline-flex; flex-wrap: wrap; align-items: center; justify-content: flex-end; gap: 8px; }
|
|
291
|
+
.spec-copy { display: inline-flex; align-items: center; align-self: center; padding: 4px 6px; }
|
|
292
|
+
.spec-actions { display: inline-flex; gap: 6px; }
|
|
293
|
+
.spec-action { padding: 3px 7px; font-size: .68rem; }
|
|
294
|
+
html[data-agent-viewed] [data-studio-action] { display: none; }
|
|
279
295
|
.spec-status { padding: 2px 5px; border: 1px solid var(--done); background: var(--done-soft); color: var(--done); font-family: var(--mono); font-size: .61rem; text-transform: uppercase; }
|
|
280
296
|
.spec-status-invalid { border-color: var(--bad); background: var(--bad-soft); color: var(--bad); }
|
|
281
297
|
|
|
@@ -375,12 +391,13 @@ ul.plain li:last-child { border-bottom: 0; }
|
|
|
375
391
|
.vault-directory > summary::-webkit-details-marker { display: none; }
|
|
376
392
|
.vault-directory > summary:hover, .vault-directory > summary[aria-current="page"] { background: var(--accent-soft); }
|
|
377
393
|
.vault-children { display: grid; gap: 1px; margin-left: 11px; padding-left: 8px; border-left: 1px solid var(--border-soft); }
|
|
378
|
-
.vault-
|
|
379
|
-
|
|
380
|
-
.vault-
|
|
381
|
-
.vault-file
|
|
382
|
-
.vault-file
|
|
383
|
-
.vault-file
|
|
394
|
+
.vault-directory[hidden] { display: none; }
|
|
395
|
+
#vault-tree-panel[data-vault-filtering] [data-vault-slot="tree"] { display: none; }
|
|
396
|
+
.vault-tree-head form { margin: 0; }
|
|
397
|
+
.vault-file { display: flex; text-decoration: none; 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; }
|
|
398
|
+
.vault-file span, .vault-directory > summary span { overflow: hidden; text-overflow: ellipsis; }
|
|
399
|
+
.vault-file:hover { background: var(--accent-soft); }
|
|
400
|
+
.vault-file[aria-current="page"] { background: var(--accent-soft); color: var(--accent); font-weight: 600; }
|
|
384
401
|
.vault-bar { display: flex; align-items: center; gap: 12px; min-height: 36px; margin-bottom: 12px; }
|
|
385
402
|
.vault-tree-toggle { flex: none; padding: 5px 10px; font-size: .78rem; }
|
|
386
403
|
.vault-breadcrumb ol { display: flex; flex-wrap: wrap; align-items: center; margin: 0; padding: 0; list-style: none; font-size: .95rem; }
|
|
@@ -460,6 +477,9 @@ ul.plain li:last-child { border-bottom: 0; }
|
|
|
460
477
|
.terminal-target { margin: 0; color: var(--muted); font-size: .8rem; overflow-wrap: anywhere; }
|
|
461
478
|
.terminal-actions { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
|
|
462
479
|
.terminal-actions .button { padding: 6px 9px; font-size: .78rem; }
|
|
480
|
+
.terminal-choice { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; font-size: .78rem; }
|
|
481
|
+
.terminal-choice[hidden] { display: none; }
|
|
482
|
+
.terminal-choice .button { padding: 6px 9px; font-size: .78rem; }
|
|
463
483
|
.terminal-status { margin: 0; color: var(--muted); font-size: 12px; }
|
|
464
484
|
.terminal-view { flex: 1; min-height: 160px; background: #000; border-radius: 8px; padding: 6px; overflow: hidden; }
|
|
465
485
|
.terminal-sessions-footer { flex: none; display: grid; gap: 6px; max-height: 28vh; overflow-y: auto; }
|
|
@@ -103,6 +103,13 @@ export function TerminalSidebar({ terminal }: { terminal: StudioTerminalPage })
|
|
|
103
103
|
Reconnect
|
|
104
104
|
</button>
|
|
105
105
|
</div>
|
|
106
|
+
<div
|
|
107
|
+
className="terminal-choice"
|
|
108
|
+
id="terminal-action-choice"
|
|
109
|
+
role="group"
|
|
110
|
+
aria-label="Choose an agent"
|
|
111
|
+
hidden
|
|
112
|
+
/>
|
|
106
113
|
<p className="terminal-status" id="terminal-status" role="status">
|
|
107
114
|
Connecting…
|
|
108
115
|
</p>
|
|
@@ -288,6 +295,16 @@ export const STUDIO_TERMINAL_SCRIPT = `(function () {
|
|
|
288
295
|
function status(text) { if (statusNode) statusNode.textContent = text; }
|
|
289
296
|
function state(name) { panel.setAttribute("data-terminal-state", name); }
|
|
290
297
|
function currentSession() { return load(${JSON.stringify(TERMINAL_SESSION_KEY)}); }
|
|
298
|
+
var root = document.documentElement;
|
|
299
|
+
var choice = document.getElementById("terminal-action-choice");
|
|
300
|
+
var AGENT_LABELS = ${JSON.stringify(AGENT_LABELS)};
|
|
301
|
+
/** Which agent this connection views, or "pending" until a stored session reattaches; the page hides run buttons while set. */
|
|
302
|
+
function viewing(agent) {
|
|
303
|
+
if (agent) root.setAttribute("data-agent-viewed", agent);
|
|
304
|
+
else root.removeAttribute("data-agent-viewed");
|
|
305
|
+
if (agent && choice) choice.hidden = true;
|
|
306
|
+
}
|
|
307
|
+
if (currentSession()) viewing("pending");
|
|
291
308
|
function size() { return { cols: term.cols, rows: term.rows }; }
|
|
292
309
|
function send(message) {
|
|
293
310
|
if (ws && ws.readyState === 1) { ws.send(JSON.stringify(message)); return true; }
|
|
@@ -317,28 +334,34 @@ export const STUDIO_TERMINAL_SCRIPT = `(function () {
|
|
|
317
334
|
try { message = JSON.parse(event.data); } catch (error) { return; }
|
|
318
335
|
if (message.type === "ready") {
|
|
319
336
|
store(${JSON.stringify(TERMINAL_SESSION_KEY)}, message.sessionId);
|
|
337
|
+
viewing(message.agent);
|
|
320
338
|
term.reset();
|
|
321
339
|
state("connected");
|
|
322
340
|
status("Connected to " + message.agent + " in " + message.companionPath + ".");
|
|
323
341
|
term.focus();
|
|
324
342
|
} else if (message.type === "exit") {
|
|
325
343
|
store(${JSON.stringify(TERMINAL_SESSION_KEY)}, null);
|
|
344
|
+
viewing(null);
|
|
326
345
|
state("detached");
|
|
327
346
|
status("The agent exited with status " + message.status + ".");
|
|
328
347
|
} else if (message.type === "ended") {
|
|
329
348
|
store(${JSON.stringify(TERMINAL_SESSION_KEY)}, null);
|
|
349
|
+
viewing(null);
|
|
330
350
|
state("detached");
|
|
331
351
|
status("The session ended" + (message.reason ? ": " + message.reason : "") + ".");
|
|
332
352
|
} else if (message.type === "detached") {
|
|
353
|
+
viewing(null);
|
|
333
354
|
state("detached");
|
|
334
355
|
status("Detached (" + message.reason + "); the agent keeps running.");
|
|
335
356
|
} else if (message.type === "takenOver") {
|
|
357
|
+
viewing(null);
|
|
336
358
|
takenOver = true;
|
|
337
359
|
state("detached");
|
|
338
360
|
status("Another tab took over this session.");
|
|
339
361
|
if (reconnectButton) reconnectButton.hidden = false;
|
|
340
362
|
} else if (message.type === "gone") {
|
|
341
363
|
store(${JSON.stringify(TERMINAL_SESSION_KEY)}, null);
|
|
364
|
+
viewing(null);
|
|
342
365
|
state("detached");
|
|
343
366
|
status("That session no longer exists.");
|
|
344
367
|
} else if (message.type === "error") {
|
|
@@ -371,12 +394,47 @@ export const STUDIO_TERMINAL_SCRIPT = `(function () {
|
|
|
371
394
|
send({ type: "resize", cols: next.cols, rows: next.rows });
|
|
372
395
|
});
|
|
373
396
|
|
|
397
|
+
/** \`task\` carries a registered action and subject; the server builds the prompt from them. */
|
|
398
|
+
function startAgent(agent, task) {
|
|
399
|
+
var dims = size();
|
|
400
|
+
var message = { type: "start", agent: agent, companion: companion, tabId: tabId, cols: dims.cols, rows: dims.rows };
|
|
401
|
+
if (task) { message.action = task.action; message.subject = task.subject; }
|
|
402
|
+
if (!send(message)) { pending = message; connect(); }
|
|
403
|
+
}
|
|
374
404
|
Array.prototype.forEach.call(document.querySelectorAll("[data-terminal-start]"), function (button) {
|
|
375
|
-
button.addEventListener("click", function () {
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
405
|
+
button.addEventListener("click", function () { startAgent(button.getAttribute("data-terminal-start")); });
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
/** The sidebar script owns collapse and drawer state, so reveal the terminal through its controls. */
|
|
409
|
+
function revealTerminal() {
|
|
410
|
+
var expand = document.getElementById("terminal-expand");
|
|
411
|
+
if (root.hasAttribute("data-terminal-collapsed") && expand) expand.click();
|
|
412
|
+
var opener = document.getElementById("terminal-drawer-open");
|
|
413
|
+
if (opener && !panel.hasAttribute("data-drawer-open") && getComputedStyle(opener).display !== "none") opener.click();
|
|
414
|
+
}
|
|
415
|
+
function showChoice(agents, task) {
|
|
416
|
+
if (!choice) return;
|
|
417
|
+
while (choice.firstChild) choice.removeChild(choice.firstChild);
|
|
418
|
+
var label = document.createElement("span");
|
|
419
|
+
label.textContent = "Run " + task.action + " with:";
|
|
420
|
+
choice.appendChild(label);
|
|
421
|
+
agents.forEach(function (agent) {
|
|
422
|
+
choice.appendChild(button(AGENT_LABELS[agent], function () {
|
|
423
|
+
choice.hidden = true;
|
|
424
|
+
startAgent(agent, task);
|
|
425
|
+
}));
|
|
379
426
|
});
|
|
427
|
+
choice.appendChild(button("Cancel", function () { choice.hidden = true; }));
|
|
428
|
+
choice.hidden = false;
|
|
429
|
+
}
|
|
430
|
+
document.addEventListener("studio:terminal-action", function (event) {
|
|
431
|
+
var task = event.detail || {};
|
|
432
|
+
var agents = (task.agents || []).filter(function (agent) { return AGENT_LABELS[agent]; });
|
|
433
|
+
if (!agents.length) return;
|
|
434
|
+
revealTerminal();
|
|
435
|
+
if (root.hasAttribute("data-agent-viewed")) { status("End the current session first."); return; }
|
|
436
|
+
if (agents.length === 1) startAgent(agents[0], task);
|
|
437
|
+
else showChoice(agents, task);
|
|
380
438
|
});
|
|
381
439
|
if (reconnectButton) reconnectButton.addEventListener("click", function () { attempts = 0; connect(); });
|
|
382
440
|
|