create-pmos 0.18.1 → 0.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +30 -0
- package/package.json +1 -1
- package/payload/KIT-VERSION +1 -1
- package/workspace/README.md +21 -1
- package/workspace/lib/four-panel.js +259 -24
- package/workspace/lib/tree-share.js +171 -13
- package/workspace/package-lock.json +2 -2
- package/workspace/package.json +4 -4
- package/workspace/public/index.html +6 -0
- package/workspace/public/pm/app.js +44 -7
- package/workspace/public/pm/data.js +42 -2
- package/workspace/public/pm/fixture.js +24 -1
- package/workspace/public/pm/panel-detail.js +226 -20
- package/workspace/public/pm/panel-tabs.js +192 -4
- package/workspace/public/pm/state.js +6 -0
- package/workspace/public/pm/tab-doc.js +5 -5
- package/workspace/public/pm/tab-task.js +1 -1
- package/workspace/public/pm/tab-term.js +17 -1
- package/workspace/server.js +87 -7
|
@@ -446,7 +446,7 @@ if (typeof window !== 'undefined') {
|
|
|
446
446
|
|
|
447
447
|
/* ── fixture pane ──────────────────────────────────────────────────────────────────────────── */
|
|
448
448
|
|
|
449
|
-
const SCROLL_STYLE = 'flex:1;overflow-y:auto;padding:14px 16px;font-family:var(--mono);font-size:12.5px;line-height:1.65;color:#c4ccce';
|
|
449
|
+
const SCROLL_STYLE = 'flex:1;min-height:0;overflow-y:auto;padding:14px 16px;font-family:var(--mono);font-size:12.5px;line-height:1.65;color:#c4ccce';
|
|
450
450
|
const LINE_STYLE = (c) => 'color:' + c + ';white-space:pre-wrap;overflow-wrap:anywhere';
|
|
451
451
|
const PROMPT_ROW_STYLE = 'display:flex;gap:8px;align-items:baseline';
|
|
452
452
|
const PROMPT_STYLE = 'color:#47cf79;flex:none';
|
|
@@ -639,6 +639,22 @@ export function sendToTerm(tab, data) {
|
|
|
639
639
|
return send({ t: 'input', id: rec.sessionId, data });
|
|
640
640
|
}
|
|
641
641
|
|
|
642
|
+
/**
|
|
643
|
+
* End a running session, by id — the `+ session` chooser's one destructive action.
|
|
644
|
+
*
|
|
645
|
+
* The wire message already existed (`t:'kill'`); what did not exist anywhere in the four-panel
|
|
646
|
+
* surface was a way to reach it, which is why "is it safe to end this?" had no answer the PM could
|
|
647
|
+
* act on. This is a bare send: the CONFIRMATION lives in the chooser, next to the reading of what
|
|
648
|
+
* ending it would cost, because that is the only place the PM is looking at both at once.
|
|
649
|
+
*
|
|
650
|
+
* Returns false when the socket is not up, so a caller can say "not sent" rather than imply a kill
|
|
651
|
+
* that never left the page.
|
|
652
|
+
*/
|
|
653
|
+
export function killSession(id) {
|
|
654
|
+
if (FIXTURE || !id) return false;
|
|
655
|
+
return send({ t: 'kill', id });
|
|
656
|
+
}
|
|
657
|
+
|
|
642
658
|
/**
|
|
643
659
|
* The SERVER's session id this tab is bound to, or null. The scratchpad reads it: a pad belongs to
|
|
644
660
|
* the session, and the session's identity is this id — not the tab id (which survives a session
|
package/workspace/server.js
CHANGED
|
@@ -369,6 +369,36 @@ function treeForWs(wsIdent) {
|
|
|
369
369
|
function treeTenants(tree, excludeId) {
|
|
370
370
|
return treeShare.coTenants([...sessions.values()], treeOfWsMap(), tree, excludeId);
|
|
371
371
|
}
|
|
372
|
+
/**
|
|
373
|
+
* A registry row, described for a human deciding whether to JOIN it rather than start another
|
|
374
|
+
* (workspace-session-choice). "2 sessions already running" names a fact the PM cannot act on; what
|
|
375
|
+
* they need is which one, what it is doing, and what ending it would cost.
|
|
376
|
+
*
|
|
377
|
+
* Every field is one the sidebar already publishes — the joined presented state, the quiet clock,
|
|
378
|
+
* the sidebar's own word for it, and the agent's last words. Nothing new is derived here and no
|
|
379
|
+
* word is re-invented: `sessionState` owns the vocabulary and `treeShare.endRisk` owns the
|
|
380
|
+
* consequence, and this function only joins them onto one row.
|
|
381
|
+
*/
|
|
382
|
+
function describePeer(s, now) {
|
|
383
|
+
const d = harness.deriveState(s.state, s.lastOutputAt, now || Date.now());
|
|
384
|
+
const hx = transcriptHarness(s);
|
|
385
|
+
const shown = sessionState.presented(d.state, hx && hx.turn);
|
|
386
|
+
const oc = hx && hx.openCall;
|
|
387
|
+
const call = oc && oc.verb
|
|
388
|
+
? { verb: oc.verb, ageMs: oc.sinceTs ? Math.max(0, (now || Date.now()) - Date.parse(oc.sinceTs)) : null }
|
|
389
|
+
: null;
|
|
390
|
+
return {
|
|
391
|
+
id: s.id, ws: s.ws, name: s.name, agent: s.agent, state: s.state, shown,
|
|
392
|
+
quietMs: d.quietMs,
|
|
393
|
+
label: sessionState.label(shown, { quietMs: d.quietMs, call, fmtDur: treeShare.ago }),
|
|
394
|
+
doing: (harnessOf(s) || {}).lastWords || null,
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
/** The same set as `treeTenants`, each row described. The order is the registry's, unchanged. */
|
|
398
|
+
function treePeersDescribed(tree, excludeId) {
|
|
399
|
+
const now = Date.now();
|
|
400
|
+
return treeTenants(tree, excludeId).map((s) => describePeer(s, now));
|
|
401
|
+
}
|
|
372
402
|
function treeWriter(tree) {
|
|
373
403
|
return treeShare.writerOf(treeClaims, treeOfWsMap(), [...sessions.values()], tree);
|
|
374
404
|
}
|
|
@@ -382,12 +412,16 @@ function announceTree(s) {
|
|
|
382
412
|
const tree = treeForWs(s.ws);
|
|
383
413
|
s.tree = tree || null;
|
|
384
414
|
if (!tree) return [];
|
|
385
|
-
|
|
415
|
+
// DESCRIBED rows, not bare registry objects: the pane line now carries what each co-tenant is
|
|
416
|
+
// doing and what ending it would cost, and it must read from the same join the sidebar does.
|
|
417
|
+
const mates = treePeersDescribed(tree, s.id);
|
|
386
418
|
const holder = treeWriter(tree);
|
|
387
419
|
if (!holder) treeClaims.set(tree, s.id); // vacant → this session declares itself
|
|
388
420
|
if (!mates.length) return []; // alone in the tree: nothing to say
|
|
389
421
|
const held = holder ? (sessions.get(holder) || {}).name : null;
|
|
390
|
-
const line = treeShare.paneAdvisory(mates, {
|
|
422
|
+
const line = treeShare.paneAdvisory(mates, {
|
|
423
|
+
role: holder ? 'reader' : 'writer', writerName: held, writerId: holder || s.id, tree,
|
|
424
|
+
});
|
|
391
425
|
if (line) appendBuf(s, line);
|
|
392
426
|
return mates;
|
|
393
427
|
}
|
|
@@ -396,7 +430,9 @@ function treeNoticeFor(s, mates) {
|
|
|
396
430
|
if (!mates || !mates.length) return null;
|
|
397
431
|
const holder = treeWriter(s.tree);
|
|
398
432
|
const held = holder && holder !== s.id ? (sessions.get(holder) || {}).name : null;
|
|
399
|
-
return treeShare.coTenancyNotice(mates, {
|
|
433
|
+
return treeShare.coTenancyNotice(mates, {
|
|
434
|
+
role: holder === s.id ? 'writer' : 'reader', writerName: held, writerId: holder, tree: s.tree,
|
|
435
|
+
});
|
|
400
436
|
}
|
|
401
437
|
function wsList() {
|
|
402
438
|
return [...workspaces.values()].map((w) => {
|
|
@@ -410,6 +446,12 @@ function wsList() {
|
|
|
410
446
|
// the client renders the first quietly and the second as a fault (R1/R2).
|
|
411
447
|
repo_key: rid.key, repo_name: rid.name, branch: rid.branch,
|
|
412
448
|
branch_error: rid.branchError, branch_code: rid.branchCode,
|
|
449
|
+
// The COLLISION UNIT (workspace-session-concurrency), published so a client can answer "who
|
|
450
|
+
// else is in the tree I am about to start in?" BEFORE it starts anything. Without it the
|
|
451
|
+
// browser could only find co-tenants via a session that already exists there, which is
|
|
452
|
+
// exactly one session too late for a chooser. Null for a folder with no git — no index to
|
|
453
|
+
// collide over, so no group.
|
|
454
|
+
tree: rid.tree || null,
|
|
413
455
|
// Where the tree came from, and the ONE label the repository list can honestly put on a row.
|
|
414
456
|
// `remote` is the raw origin URL (null when there is none) so a caller can derive a richer
|
|
415
457
|
// answer without guessing; `kind` is the surface's own two-value vocabulary — a repository
|
|
@@ -1559,6 +1601,17 @@ function sessionList() {
|
|
|
1559
1601
|
// `failed` does not exist (no observable for it). The client re-derives this from the SAME
|
|
1560
1602
|
// module — see /lib/session-state.js below — so the two can never hold different opinions.
|
|
1561
1603
|
shown: sessionState.presented(d.state, hx && hx.turn), quietMs: d.quietMs,
|
|
1604
|
+
// The rendered word for that state — computed HERE rather than in each client, because it is
|
|
1605
|
+
// the same join (`sessionState.label`) with the same clock, and a client re-deriving it was
|
|
1606
|
+
// already producing a second, slightly different sentence for one session.
|
|
1607
|
+
label: sessionState.label(sessionState.presented(d.state, hx && hx.turn), {
|
|
1608
|
+
quietMs: d.quietMs,
|
|
1609
|
+
call: hx && hx.openCall && hx.openCall.verb
|
|
1610
|
+
? { verb: hx.openCall.verb,
|
|
1611
|
+
ageMs: hx.openCall.sinceTs ? Math.max(0, now - Date.parse(hx.openCall.sinceTs)) : null }
|
|
1612
|
+
: null,
|
|
1613
|
+
fmtDur: treeShare.ago,
|
|
1614
|
+
}),
|
|
1562
1615
|
lastOutputAt: s.lastOutputAt || null,
|
|
1563
1616
|
lastWords: h.lastWords,
|
|
1564
1617
|
// the harness proper — structured record, not rendered bytes. Resolved ONCE above and
|
|
@@ -2115,7 +2168,7 @@ function spawnWithBranch({ name, agent, cwd, ws, branch, createBranch, isolate,
|
|
|
2115
2168
|
// Both facts travel: `notice` is a single string on the wire and a branch notice and a co-tenancy
|
|
2116
2169
|
// notice can both be true of one spawn. joinNotices keeps them both rather than letting the
|
|
2117
2170
|
// second evict the first exactly when the PM most needs both.
|
|
2118
|
-
const shared = treeNoticeFor(s,
|
|
2171
|
+
const shared = treeNoticeFor(s, treePeersDescribed(s.tree, s.id));
|
|
2119
2172
|
return { ok: true, session: s, notice: treeShare.joinNotices(notice, shared) };
|
|
2120
2173
|
}
|
|
2121
2174
|
|
|
@@ -3091,6 +3144,7 @@ async function handleRequest(req, res) {
|
|
|
3091
3144
|
if (p === '/lib/layout.js') return serveFile(res, path.join(__dirname, 'lib', 'layout.js'));
|
|
3092
3145
|
if (p === '/lib/mobile-nav.js') return serveFile(res, path.join(__dirname, 'lib', 'mobile-nav.js'));
|
|
3093
3146
|
if (p === '/lib/session-state.js') return serveFile(res, path.join(__dirname, 'lib', 'session-state.js'));
|
|
3147
|
+
if (p === '/lib/tree-share.js') return serveFile(res, path.join(__dirname, 'lib', 'tree-share.js'));
|
|
3094
3148
|
// The editor computes its live diff with the SAME module the server saves through, so what the PM
|
|
3095
3149
|
// sees while typing and what gets written cannot disagree. NB: /lib/ is an explicit allowlist, not
|
|
3096
3150
|
// a directory route — a <script src> for a file missing from this list 404s silently, and the
|
|
@@ -3630,9 +3684,13 @@ async function handleRequest(req, res) {
|
|
|
3630
3684
|
note: 'this workspace is not inside a git working tree — there is no shared index to collide over' });
|
|
3631
3685
|
}
|
|
3632
3686
|
const writer = treeWriter(tree);
|
|
3633
|
-
|
|
3634
|
-
|
|
3687
|
+
// Described, not merely listed (workspace-session-choice): a caller deciding between joining a
|
|
3688
|
+
// running session and starting another needs the state, the word for it, what the agent is
|
|
3689
|
+
// doing and what ending it would cost — the same four facts the chooser and the pane line show.
|
|
3690
|
+
const rows = treePeersDescribed(tree, null).map((s) => ({
|
|
3691
|
+
...s,
|
|
3635
3692
|
role: treeShare.roleOf(s.id, tree, writer),
|
|
3693
|
+
risk: treeShare.endRisk(s.shown, s.quietMs),
|
|
3636
3694
|
}));
|
|
3637
3695
|
return json(res, { ok: true, tree, writer, sessions: rows, advisory: true,
|
|
3638
3696
|
note: writer
|
|
@@ -3792,6 +3850,11 @@ async function handleRequest(req, res) {
|
|
|
3792
3850
|
: null,
|
|
3793
3851
|
artifactLinks: fourPanel.readDeclarations(stateDir, fourPanel.ARTIFACT_LINKS),
|
|
3794
3852
|
initiativeLinks: fourPanel.readDeclarations(stateDir, fourPanel.INITIATIVE_LINKS),
|
|
3853
|
+
// Which destinations a session could look up, derived from the PM's agent profiles. The home
|
|
3854
|
+
// directory is passed IN rather than read inside four-panel.js for the same reason
|
|
3855
|
+
// readProfiles takes it as a parameter: a module that reads os.homedir() itself can only be
|
|
3856
|
+
// tested against the machine running the suite.
|
|
3857
|
+
homeDir: os.homedir(),
|
|
3795
3858
|
});
|
|
3796
3859
|
// Discriminated READ results stay 200 — the body carries the reason, the transport code cannot.
|
|
3797
3860
|
return json(res, { ws: w ? w.id : null, ...out });
|
|
@@ -3819,7 +3882,20 @@ async function handleRequest(req, res) {
|
|
|
3819
3882
|
built.row,
|
|
3820
3883
|
);
|
|
3821
3884
|
// A write that failed returns 500 and says so — never a false green (the /api/gate rule).
|
|
3822
|
-
return json(res, { ...wrote, connectors: fourPanel.connectors() }, wrote.ok ? 200 : 500);
|
|
3885
|
+
return json(res, { ...wrote, connectors: fourPanel.connectors(os.homedir()) }, wrote.ok ? 200 : 500);
|
|
3886
|
+
}
|
|
3887
|
+
if (p === '/api/initiative/lookup-task' && req.method === 'POST') {
|
|
3888
|
+
// The task a SESSION runs to resolve a reference. This endpoint composes a string and returns
|
|
3889
|
+
// it. It contacts nothing — that is the entire point of the design: PMOS describes the lookup,
|
|
3890
|
+
// an agent session holding the PM's own MCP performs it, and the result comes back through
|
|
3891
|
+
// /api/initiative/link like any other declaration. There is no branch here that reaches out.
|
|
3892
|
+
let body = {};
|
|
3893
|
+
try { body = JSON.parse((await readBody(req)) || '{}'); }
|
|
3894
|
+
catch { return json(res, { ok: false, error: 'invalid JSON body' }, 400); }
|
|
3895
|
+
const built = fourPanel.lookupTask({
|
|
3896
|
+
kind: body.kind, subject: body.subject, hint: body.hint, profile: body.profile,
|
|
3897
|
+
});
|
|
3898
|
+
return json(res, built, built.ok ? 200 : 400);
|
|
3823
3899
|
}
|
|
3824
3900
|
if (p === '/api/search') {
|
|
3825
3901
|
// Episodic recall over this workspace's own past sessions. Localhost only, like everything
|
|
@@ -4406,6 +4482,10 @@ module.exports = { server, spawnSession, removeSession, renameSession, sessions,
|
|
|
4406
4482
|
// (one repo registered twice is one tree; two worktrees of one repo are two) without driving
|
|
4407
4483
|
// every case through a spawn.
|
|
4408
4484
|
treeForWs, treeTenants, treeWriter, treeClaims,
|
|
4485
|
+
// …and the DESCRIBED form, exported for the same reason: "what is that session doing and what
|
|
4486
|
+
// would ending it cost?" is the half of co-tenancy the PM actually acts on, so the suite asserts
|
|
4487
|
+
// it directly rather than reading it back out of a rendered sentence.
|
|
4488
|
+
describePeer, treePeersDescribed,
|
|
4409
4489
|
// exported so the suite can assert the D08 invariant end-to-end: the harness parsers consume
|
|
4410
4490
|
// scrubbed text, so a secret printed into scrollback must not survive into a rendered value.
|
|
4411
4491
|
scrub, harnessOf,
|