mulmoterminal 5.0.0 → 5.0.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
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mulmoterminal",
|
|
3
|
-
"version": "5.0.
|
|
3
|
+
"version": "5.0.1",
|
|
4
4
|
"description": "Run multiple Claude Code and Codex sessions in parallel — a browser terminal grid that shows which agent needs you. Local, tmux-backed.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -20,7 +20,15 @@ import { emptyTranscriptScan, foldTranscriptView, trackTurnStarts, transcriptVie
|
|
|
20
20
|
import type { TranscriptPage, TranscriptView } from "../../common/transcriptView.js";
|
|
21
21
|
import { createCodexFold } from "./transcript-view-codex.js";
|
|
22
22
|
import { createCursorFold } from "./transcript-view-cursor.js";
|
|
23
|
-
import {
|
|
23
|
+
import {
|
|
24
|
+
antigravityConversations,
|
|
25
|
+
antigravityConversationsHydrated,
|
|
26
|
+
codexRollouts,
|
|
27
|
+
codexRolloutsHydrated,
|
|
28
|
+
museConversations,
|
|
29
|
+
museConversationsHydrated,
|
|
30
|
+
} from "./registry.js";
|
|
31
|
+
import { grokConversationExists, grokSessionsRoot } from "../agents/grok-session.js";
|
|
24
32
|
import { codexSessionsRoot } from "../agents/codex-session.js";
|
|
25
33
|
import { codexRolloutPath } from "../agents/codex-sessions.js";
|
|
26
34
|
import { cursorTranscriptPath } from "../agents/cursor-sessions.js";
|
|
@@ -333,7 +341,68 @@ const TRANSCRIPT_SOURCES: readonly TranscriptSource[] = [claudeSource, codexSour
|
|
|
333
341
|
* Derived from the source list rather than listed, so wiring a source removes it from here by
|
|
334
342
|
* construction. `shell` is excluded deliberately — a shell cell has no conversation and never
|
|
335
343
|
* will, so the screen IS its content rather than a fallback from something missing. */
|
|
336
|
-
|
|
344
|
+
/** Whether a reader here answers for this agent. Exported for the spec that pins `UNREAD_SOURCES`
|
|
345
|
+
* as its exact complement: asserted against a hand-written list instead, the assertion passes when
|
|
346
|
+
* a reader is wired and the agent is left in both lists — which is the one drift it exists to catch,
|
|
347
|
+
* and Codex demonstrated it passing (round 2). Derived from the source list either way, so there is
|
|
348
|
+
* no second place to keep in step. */
|
|
349
|
+
export const hasReader = (agent: SessionAgent): boolean => TRANSCRIPT_SOURCES.some((source) => source.agent === agent);
|
|
350
|
+
|
|
351
|
+
/** The agents with no reader, and how to ask their own stores whether they hold a session (#2116).
|
|
352
|
+
*
|
|
353
|
+
* This exists because the caller's `agentOf` answers from the PROCESS — a live pty, or what tmux
|
|
354
|
+
* reports the pane is running — and both are gone once a session ends. From that moment a grok,
|
|
355
|
+
* muse or antigravity session was told "nothing has been written yet", which is a different and
|
|
356
|
+
* false sentence: their conversation exists, this host just cannot read it.
|
|
357
|
+
*
|
|
358
|
+
* So the same principle the reader is built on applies here too: ASK THE STORE, NOT THE PROCESS.
|
|
359
|
+
* Two of the three answer from a map this server already hydrated off its own append log, which
|
|
360
|
+
* costs nothing; grok keeps no such log, because a grok conversation id IS the session id, so its
|
|
361
|
+
* question is one directory.
|
|
362
|
+
*
|
|
363
|
+
* Asked ONLY when every reader missed AND the process could not say — a live shell cell answers
|
|
364
|
+
* `shell` from its pty and never reaches here.
|
|
365
|
+
*
|
|
366
|
+
* THE LIST MUST BE EXACTLY THE AGENTS `hasReader` SAYS NO TO. When #1822 gives one of them a
|
|
367
|
+
* reader, leaving it here would make a readable session report that it cannot be read; a spec pins
|
|
368
|
+
* the two lists against each other rather than trusting a comment. */
|
|
369
|
+
export interface UnreadSource {
|
|
370
|
+
agent: SessionAgent;
|
|
371
|
+
holds: (cwd: string, id: string) => Promise<boolean>;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
export const UNREAD_SOURCES: readonly UnreadSource[] = [
|
|
375
|
+
{
|
|
376
|
+
agent: "antigravity",
|
|
377
|
+
holds: async (_cwd, id) => {
|
|
378
|
+
await antigravityConversationsHydrated; // a request racing boot must not read an empty map
|
|
379
|
+
return antigravityConversations.has(id);
|
|
380
|
+
},
|
|
381
|
+
},
|
|
382
|
+
{
|
|
383
|
+
agent: "muse",
|
|
384
|
+
holds: async (_cwd, id) => {
|
|
385
|
+
await museConversationsHydrated;
|
|
386
|
+
return museConversations.has(id);
|
|
387
|
+
},
|
|
388
|
+
},
|
|
389
|
+
// The cwd IS the question for grok: its sessions are filed per directory, and the one on screen is
|
|
390
|
+
// the only one this cell could be showing.
|
|
391
|
+
{ agent: "grok", holds: (cwd, id) => Promise.resolve(grokConversationExists(grokSessionsRoot(), cwd, id)) },
|
|
392
|
+
];
|
|
393
|
+
|
|
394
|
+
/** Which reader-less agent holds this session, or null when none does.
|
|
395
|
+
*
|
|
396
|
+
* The sources are a PARAMETER for the same reason the byte window is one: two of the three answer
|
|
397
|
+
* from a map this process hydrates from `~/.mulmoterminal`, and a spec that planted an entry there
|
|
398
|
+
* the way production does would be writing into the machine's real log — which is exactly what the
|
|
399
|
+
* first version of its spec did. Injected, the same test says the same thing and touches nothing. */
|
|
400
|
+
export async function unreadOwner(cwd: string, id: string, sources: readonly UnreadSource[] = UNREAD_SOURCES): Promise<SessionAgent | null> {
|
|
401
|
+
for (const source of sources) {
|
|
402
|
+
if (await source.holds(cwd, id)) return source.agent;
|
|
403
|
+
}
|
|
404
|
+
return null;
|
|
405
|
+
}
|
|
337
406
|
|
|
338
407
|
// ── the cursor a client pages with (#2112) ───────────────────────────────────────────────────
|
|
339
408
|
//
|
|
@@ -443,6 +512,11 @@ async function pageFromFile(
|
|
|
443
512
|
* Injected rather than imported: `agentOfSession` lives in server/index.ts, which imports this. */
|
|
444
513
|
export interface TranscriptViewDeps {
|
|
445
514
|
window?: TranscriptWindow;
|
|
515
|
+
/** The reader-less agents' stores, for the one question `agentOf` cannot answer once the process
|
|
516
|
+
* is gone (#2116). Injected like the window above, and for the same reason: the real ones are
|
|
517
|
+
* maps hydrated from `~/.mulmoterminal`, so a spec exercising them through production's own
|
|
518
|
+
* writer would append to the machine's real log. */
|
|
519
|
+
unreadSources?: readonly UnreadSource[];
|
|
446
520
|
/** This session's agent, when the host knows it. Consulted ONLY after every source has missed —
|
|
447
521
|
* never to choose a reader (see TranscriptSource). */
|
|
448
522
|
agentOf?: (id: string) => SessionAgent | null;
|
|
@@ -509,7 +583,11 @@ export async function sessionTranscriptPage(
|
|
|
509
583
|
// The old signature took the window positionally and several specs still do. Kept rather than
|
|
510
584
|
// migrated: the window is the only thing those specs vary, and a second parameter shape is
|
|
511
585
|
// cheaper than touching every one of them.
|
|
512
|
-
const {
|
|
586
|
+
const {
|
|
587
|
+
window = DEFAULT_TRANSCRIPT_WINDOW,
|
|
588
|
+
agentOf,
|
|
589
|
+
unreadSources = UNREAD_SOURCES,
|
|
590
|
+
} = "tailBytes" in deps ? { window: deps, agentOf: undefined, unreadSources: undefined } : deps;
|
|
513
591
|
if (!cwd || !SESSION_ID_RE.test(id)) return { view: { status: "none" }, older: null };
|
|
514
592
|
// Before any file is opened. `/clear` makes claude mint a new id and a new transcript while hooks
|
|
515
593
|
// keep reporting under ours, so from that moment `${id}.jsonl` holds the conversation the user
|
|
@@ -531,6 +609,9 @@ export async function sessionTranscriptPage(
|
|
|
531
609
|
// sentences — never to choose a reader. Asking here is safe precisely because it is last: the
|
|
532
610
|
// restarted-claude case (reported as `shell`) has already been answered by claude's file.
|
|
533
611
|
const agent = agentOf?.(id) ?? null;
|
|
534
|
-
|
|
535
|
-
|
|
612
|
+
if (agent !== null) return { view: agent !== "shell" && !hasReader(agent) ? { status: "not-supported" } : { status: "none" }, older: null };
|
|
613
|
+
// The process is gone, so it cannot say what it was. The stores of the reader-less agents can, and
|
|
614
|
+
// they outlive it (#2116).
|
|
615
|
+
const owner = await unreadOwner(cwd, id, unreadSources);
|
|
616
|
+
return { view: owner === null ? { status: "none" } : { status: "not-supported" }, older: null };
|
|
536
617
|
}
|