opencode-crosstalk 0.0.0-stage → 0.2.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/LICENSE +21 -0
- package/README.md +460 -2
- package/assets/CursorGuard.png +0 -0
- package/assets/Wren.png +0 -0
- package/assets/avatars/ATTRIBUTION.md +18 -0
- package/assets/avatars/female/01-explorer-eeb4a4.png +0 -0
- package/assets/avatars/female/02-manager-b16a5b.png +0 -0
- package/assets/avatars/female/02-manager-e7a391.png +0 -0
- package/assets/avatars/female/03-coder-92594b.png +0 -0
- package/assets/avatars/female/03-coder-e5a07e.png +0 -0
- package/assets/avatars/female/05-analyst-b16a5b.png +0 -0
- package/assets/avatars/female/05-analyst-eeb4a4.png +0 -0
- package/assets/avatars/female/08-tester-d78774.png +0 -0
- package/assets/avatars/female/08-tester-eeb4a4.png +0 -0
- package/assets/avatars/female/10-builder-92594b.png +0 -0
- package/assets/avatars/female/10-builder-e5a07e.png +0 -0
- package/assets/avatars/female/13-researcher-92594b.png +0 -0
- package/assets/avatars/female/13-researcher-e7a391.png +0 -0
- package/assets/avatars/female/14-writer-623d36.png +0 -0
- package/assets/avatars/female/14-writer-e5a07e.png +0 -0
- package/assets/avatars/female/16-fixer-e7a391.png +0 -0
- package/assets/avatars/female/17-deployer-92594b.png +0 -0
- package/assets/avatars/female/17-deployer-e5a07e.png +0 -0
- package/assets/avatars/female/23-mentor-b16a5b.png +0 -0
- package/assets/avatars/female/23-mentor-e7a391.png +0 -0
- package/assets/avatars/female/25-pioneer-d78774.png +0 -0
- package/assets/avatars/female/26-strategist-b16a5b.png +0 -0
- package/assets/avatars/female/31-gardener-e5a07e.png +0 -0
- package/assets/avatars/female/33-simplifier-b16a5b.png +0 -0
- package/assets/avatars/female/33-simplifier-eeb4a4.png +0 -0
- package/assets/avatars/female/34-optimizer-92594b.png +0 -0
- package/assets/avatars/female/34-optimizer-e7a391.png +0 -0
- package/assets/avatars/female/36-integrator-d78774.png +0 -0
- package/assets/avatars/female/36-integrator-eeb4a4.png +0 -0
- package/assets/avatars/female/37-compiler-b16a5b.png +0 -0
- package/assets/avatars/female/37-compiler-e7a391.png +0 -0
- package/assets/avatars/female/40-formatter-b16a5b.png +0 -0
- package/assets/avatars/female/42-shipper-623d36.png +0 -0
- package/assets/avatars/female/46-steward-623d36.png +0 -0
- package/assets/avatars/female/46-steward-d78774.png +0 -0
- package/assets/avatars/female/47-shepherd-b16a5b.png +0 -0
- package/assets/avatars/female/47-shepherd-eeb4a4.png +0 -0
- package/assets/avatars/female/48-tinkerer-92594b.png +0 -0
- package/assets/avatars/female/50-wrangler-eeb4a4.png +0 -0
- package/assets/avatars/index.html +4 -0
- package/assets/avatars/male/01-explorer-d78774.png +0 -0
- package/assets/avatars/male/06-planner-92594b.png +0 -0
- package/assets/avatars/male/06-planner-e7a391.png +0 -0
- package/assets/avatars/male/07-reviewer-623d36.png +0 -0
- package/assets/avatars/male/07-reviewer-e5a07e.png +0 -0
- package/assets/avatars/male/09-migrator-b16a5b.png +0 -0
- package/assets/avatars/male/09-migrator-e7a391.png +0 -0
- package/assets/avatars/male/11-debugger-623d36.png +0 -0
- package/assets/avatars/male/11-debugger-d78774.png +0 -0
- package/assets/avatars/male/12-designer-b16a5b.png +0 -0
- package/assets/avatars/male/12-designer-eeb4a4.png +0 -0
- package/assets/avatars/male/15-editor-d78774.png +0 -0
- package/assets/avatars/male/15-editor-eeb4a4.png +0 -0
- package/assets/avatars/male/16-fixer-b16a5b.png +0 -0
- package/assets/avatars/male/18-scout-623d36.png +0 -0
- package/assets/avatars/male/18-scout-d78774.png +0 -0
- package/assets/avatars/male/19-architect-b16a5b.png +0 -0
- package/assets/avatars/male/19-architect-eeb4a4.png +0 -0
- package/assets/avatars/male/20-operator-92594b.png +0 -0
- package/assets/avatars/male/20-operator-e7a391.png +0 -0
- package/assets/avatars/male/21-coordinator-623d36.png +0 -0
- package/assets/avatars/male/21-coordinator-e5a07e.png +0 -0
- package/assets/avatars/male/22-auditor-d78774.png +0 -0
- package/assets/avatars/male/22-auditor-eeb4a4.png +0 -0
- package/assets/avatars/male/25-pioneer-623d36.png +0 -0
- package/assets/avatars/male/26-strategist-eeb4a4.png +0 -0
- package/assets/avatars/male/28-librarian-623d36.png +0 -0
- package/assets/avatars/male/28-librarian-e5a07e.png +0 -0
- package/assets/avatars/male/30-tracker-b16a5b.png +0 -0
- package/assets/avatars/male/30-tracker-e7a391.png +0 -0
- package/assets/avatars/male/31-gardener-92594b.png +0 -0
- package/assets/avatars/male/32-refactorer-623d36.png +0 -0
- package/assets/avatars/male/32-refactorer-d78774.png +0 -0
- package/assets/avatars/male/35-profiler-623d36.png +0 -0
- package/assets/avatars/male/35-profiler-e5a07e.png +0 -0
- package/assets/avatars/male/38-parser-92594b.png +0 -0
- package/assets/avatars/male/38-parser-e5a07e.png +0 -0
- package/assets/avatars/male/39-linter-623d36.png +0 -0
- package/assets/avatars/male/39-linter-d78774.png +0 -0
- package/assets/avatars/male/40-formatter-eeb4a4.png +0 -0
- package/assets/avatars/male/41-patcher-92594b.png +0 -0
- package/assets/avatars/male/41-patcher-e7a391.png +0 -0
- package/assets/avatars/male/42-shipper-e5a07e.png +0 -0
- package/assets/avatars/male/43-maintainer-d78774.png +0 -0
- package/assets/avatars/male/43-maintainer-eeb4a4.png +0 -0
- package/assets/avatars/male/44-guardian-b16a5b.png +0 -0
- package/assets/avatars/male/44-guardian-e7a391.png +0 -0
- package/assets/avatars/male/45-sentinel-92594b.png +0 -0
- package/assets/avatars/male/45-sentinel-e5a07e.png +0 -0
- package/assets/avatars/male/48-tinkerer-e7a391.png +0 -0
- package/assets/avatars/male/49-hacker-623d36.png +0 -0
- package/assets/avatars/male/49-hacker-e5a07e.png +0 -0
- package/assets/avatars/male/50-wrangler-d78774.png +0 -0
- package/assets/avatars/manifest.json +1202 -0
- package/index.ts +11 -0
- package/package.json +65 -4
- package/src/config.ts +176 -0
- package/src/core/avatar.ts +92 -0
- package/src/core/claims.ts +302 -0
- package/src/core/clock.ts +31 -0
- package/src/core/directory.ts +48 -0
- package/src/core/format.ts +382 -0
- package/src/core/mailbox.ts +239 -0
- package/src/core/mesh.ts +781 -0
- package/src/core/registry.ts +306 -0
- package/src/deliverer.ts +86 -0
- package/src/events.ts +66 -0
- package/src/index.ts +323 -0
- package/src/rpc.ts +52 -0
- package/src/storage.ts +105 -0
- package/src/tools/args.ts +132 -0
- package/src/tools/claim.ts +87 -0
- package/src/tools/heartbeat.ts +152 -0
- package/src/tools/inbox.ts +76 -0
- package/src/tools/index.ts +62 -0
- package/src/tools/peers.ts +101 -0
- package/src/tools/send.ts +84 -0
- package/src/tools/status.ts +137 -0
- package/src/tools/types.ts +72 -0
- package/src/tools/wait.ts +96 -0
- package/src/tui/avatar.ts +188 -0
- package/src/tui/index.tsx +216 -0
- package/src/types.ts +147 -0
- package/tui.ts +8 -0
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview CLI (TUI) plugin: the session avatar in the sidebar, with the
|
|
3
|
+
* session's live status summary under it.
|
|
4
|
+
*
|
|
5
|
+
* The server plugin owns the directory (who declared what); this side renders
|
|
6
|
+
* it. The RPC contract is shared as a plain object, so that no runtime plugin
|
|
7
|
+
* module has to resolve in the TUI process.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { readFileSync } from "node:fs"
|
|
11
|
+
import { Plugin, usePlugin } from "@opencode/plugin/tui"
|
|
12
|
+
import { Show, createMemo, createSignal } from "solid-js"
|
|
13
|
+
import { TextAttributes } from "@opentui/core"
|
|
14
|
+
import type { DirectoryEntry, DirectoryPayload } from "../core/directory.ts"
|
|
15
|
+
import { normalizeResource } from "../core/claims.ts"
|
|
16
|
+
import { CrosstalkRpc } from "../rpc.ts"
|
|
17
|
+
import { emoticonFor, entryForPortrait, isSummaryStale, summaryText, titleWithEmoticon, type AvatarEntry } from "./avatar.ts"
|
|
18
|
+
|
|
19
|
+
const manifestUrl = new URL("../../assets/avatars/manifest.json", import.meta.url)
|
|
20
|
+
|
|
21
|
+
/** Width of the avatar art, and so of the column the summary wraps inside. */
|
|
22
|
+
const AVATAR_WIDTH = 29
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* How often the sidebar re-checks whether a summary went stale. The directory
|
|
26
|
+
* is only rebuilt on mesh mutations, and a session nobody talks to emits
|
|
27
|
+
* none — without this tick an untouched summary could stay undimmed forever.
|
|
28
|
+
*/
|
|
29
|
+
const SUMMARY_TICK_MS = 30_000
|
|
30
|
+
|
|
31
|
+
function loadEntries(): AvatarEntry[] {
|
|
32
|
+
try {
|
|
33
|
+
const parsed = JSON.parse(readFileSync(manifestUrl, "utf8")) as { entries?: AvatarEntry[] }
|
|
34
|
+
return Array.isArray(parsed.entries) ? parsed.entries : []
|
|
35
|
+
} catch {
|
|
36
|
+
return []
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const sourceCache = new Map<string, URL>()
|
|
41
|
+
|
|
42
|
+
function sourceFor(entry: AvatarEntry): URL {
|
|
43
|
+
const key = `${entry.pool}/${entry.file}`
|
|
44
|
+
let url = sourceCache.get(key)
|
|
45
|
+
if (!url) {
|
|
46
|
+
url = new URL(`../../assets/avatars/${key}`, import.meta.url)
|
|
47
|
+
sourceCache.set(key, url)
|
|
48
|
+
}
|
|
49
|
+
return url
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function asPayload(value: unknown): DirectoryPayload {
|
|
53
|
+
const sessions = (value as { sessions?: unknown } | undefined)?.sessions
|
|
54
|
+
return { sessions: Array.isArray(sessions) ? (sessions as DirectoryEntry[]) : [] }
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Whether a session sits in this TUI's directory or below it, compared with the
|
|
59
|
+
* same normalisation claims use so a Windows worktree does not look like a
|
|
60
|
+
* different place because of a separator or a letter case.
|
|
61
|
+
*/
|
|
62
|
+
function inPlace(candidate: string | undefined, home: string): boolean {
|
|
63
|
+
if (candidate === undefined) return false
|
|
64
|
+
const base = normalizeResource(home)
|
|
65
|
+
const other = normalizeResource(candidate)
|
|
66
|
+
return other === base || other.startsWith(`${base}/`)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function AvatarLine(props: {
|
|
70
|
+
sessionID: string
|
|
71
|
+
directory: () => DirectoryPayload
|
|
72
|
+
entries: readonly AvatarEntry[]
|
|
73
|
+
/** Starts a fresh staleness evaluation; see `SUMMARY_TICK_MS`. */
|
|
74
|
+
tick: () => number
|
|
75
|
+
}) {
|
|
76
|
+
const context = usePlugin()
|
|
77
|
+
const [failed, setFailed] = createSignal(false)
|
|
78
|
+
const session = () => props.directory().sessions.find((entry) => entry.sessionID === props.sessionID)
|
|
79
|
+
// The live status line under the art: one or two short sentences on what the
|
|
80
|
+
// session is doing right now, re-read from the directory so every refreshed
|
|
81
|
+
// declaration shows up without a reload.
|
|
82
|
+
const summary = () => summaryText(session())
|
|
83
|
+
// Dimmed once the summary stops being refreshed: read on the tick so the dim
|
|
84
|
+
// arrives on its own, without waiting for the next directory push.
|
|
85
|
+
const stale = createMemo(() => {
|
|
86
|
+
props.tick()
|
|
87
|
+
return isSummaryStale(session()?.summaryAt, Date.now())
|
|
88
|
+
})
|
|
89
|
+
// The server froze the portrait when the session first declared a task;
|
|
90
|
+
// a session without one has no task and shows no avatar. A portrait whose
|
|
91
|
+
// art left the manifest is not swapped for another — it shows nothing.
|
|
92
|
+
const picked = createMemo(() => entryForPortrait(props.entries, session()?.portrait))
|
|
93
|
+
// Stable source: a fresh URL on every directory update would make the
|
|
94
|
+
// renderable re-decode the PNG on each change event.
|
|
95
|
+
const source = createMemo(() => {
|
|
96
|
+
const entry = picked()
|
|
97
|
+
return entry ? sourceFor(entry) : undefined
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
return (
|
|
101
|
+
<Show when={picked()}>
|
|
102
|
+
{() => (
|
|
103
|
+
<box flexDirection="column" alignItems="center" gap={0}>
|
|
104
|
+
{/*
|
|
105
|
+
Blocks on purpose: the sidebar sits in a scrollbox and the terminal
|
|
106
|
+
we target (VS Code) announces sixel but does not paint it, so cell
|
|
107
|
+
drawing is the portable path. On kitty-graphics terminals this can
|
|
108
|
+
be switched to "auto".
|
|
109
|
+
*/}
|
|
110
|
+
<image
|
|
111
|
+
source={source()}
|
|
112
|
+
width={AVATAR_WIDTH}
|
|
113
|
+
height={15}
|
|
114
|
+
fit="cover"
|
|
115
|
+
protocol="blocks"
|
|
116
|
+
onError={() => setFailed(true)}
|
|
117
|
+
/>
|
|
118
|
+
<text fg={context.theme.text.base}>{`${emoticonFor(picked())} ${session()?.name ?? "crosstalk"}`}</text>
|
|
119
|
+
<text fg={context.theme.text.muted}>{session()?.role ?? ""}</text>
|
|
120
|
+
<Show when={summary()}>
|
|
121
|
+
{() => (
|
|
122
|
+
<text
|
|
123
|
+
fg={context.theme.text.muted}
|
|
124
|
+
width={AVATAR_WIDTH}
|
|
125
|
+
wrapMode="word"
|
|
126
|
+
attributes={stale() ? TextAttributes.DIM : TextAttributes.NONE}
|
|
127
|
+
>
|
|
128
|
+
{summary()}
|
|
129
|
+
</text>
|
|
130
|
+
)}
|
|
131
|
+
</Show>
|
|
132
|
+
<Show when={failed()}>
|
|
133
|
+
<text fg={context.theme.text.muted}>[image failed]</text>
|
|
134
|
+
</Show>
|
|
135
|
+
</box>
|
|
136
|
+
)}
|
|
137
|
+
</Show>
|
|
138
|
+
)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export default Plugin.define({
|
|
142
|
+
id: "opencode.crosstalk.tui",
|
|
143
|
+
async setup(context) {
|
|
144
|
+
const entries = loadEntries()
|
|
145
|
+
const [directory, setDirectory] = createSignal<DirectoryPayload>({ sessions: [] })
|
|
146
|
+
// Slow tick for the summary's staleness dim; see `SUMMARY_TICK_MS`.
|
|
147
|
+
const [tick, setTick] = createSignal(0)
|
|
148
|
+
const summaryClock = setInterval(() => setTick((value) => value + 1), SUMMARY_TICK_MS)
|
|
149
|
+
summaryClock.unref?.()
|
|
150
|
+
const rpc = context.client.rpc(CrosstalkRpc)
|
|
151
|
+
const location = context.location ?? context.data.location.default()
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* The tab header has no slot a plugin can render into — its label is the
|
|
155
|
+
* session title — so the emoticon is written there instead.
|
|
156
|
+
*
|
|
157
|
+
* Deliberately narrow: only a session that declared a crosstalk name, and
|
|
158
|
+
* only one in this TUI's directory or below it, may be renamed; `titleWithEmoticon`
|
|
159
|
+
* returns nothing to write once the title already carries the glyph, so the
|
|
160
|
+
* `session.renamed` event this triggers comes back as a no-op instead of a
|
|
161
|
+
* loop. Anything the user titled themselves is left as they wrote it.
|
|
162
|
+
*/
|
|
163
|
+
const written = new Map<string, string>()
|
|
164
|
+
async function decorate(payload: DirectoryPayload): Promise<void> {
|
|
165
|
+
const home = location?.directory
|
|
166
|
+
if (home === undefined) return
|
|
167
|
+
for (const entry of payload.sessions) {
|
|
168
|
+
if (entry.name === undefined || !inPlace(entry.directory, home)) continue
|
|
169
|
+
// The glyph freezes with the portrait: a later role change never
|
|
170
|
+
// silently rewrites the user's tab title.
|
|
171
|
+
const portrait = entryForPortrait(entries, entry.portrait)
|
|
172
|
+
if (portrait === undefined) continue
|
|
173
|
+
const wanted = titleWithEmoticon(entry.title, emoticonFor(portrait), entry.name)
|
|
174
|
+
if (wanted === undefined || written.get(entry.sessionID) === wanted) continue
|
|
175
|
+
written.set(entry.sessionID, wanted)
|
|
176
|
+
try {
|
|
177
|
+
await context.client.session.update({ sessionID: entry.sessionID, title: wanted })
|
|
178
|
+
} catch {
|
|
179
|
+
// Gone, or not ours to rename: forget the attempt and let the next
|
|
180
|
+
// directory update retry it.
|
|
181
|
+
written.delete(entry.sessionID)
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const apply = (payload: unknown) => {
|
|
187
|
+
const next = asPayload(payload)
|
|
188
|
+
setDirectory(next)
|
|
189
|
+
void decorate(next)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
try {
|
|
193
|
+
apply(await rpc.directory({}, { location }))
|
|
194
|
+
} catch {
|
|
195
|
+
// The sidebar stays empty until the next change event arrives.
|
|
196
|
+
}
|
|
197
|
+
const unsubscribe = rpc.events.on("changed", (event) => {
|
|
198
|
+
const eventDirectory = (event as { location?: { directory?: string } }).location?.directory
|
|
199
|
+
if (location?.directory !== undefined && eventDirectory !== location.directory) return
|
|
200
|
+
apply(event.data)
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
const unregister = context.ui.slot({
|
|
204
|
+
append: "sidebar.content",
|
|
205
|
+
render: (input) => (
|
|
206
|
+
<AvatarLine sessionID={input.sessionID} directory={directory} entries={entries} tick={tick} />
|
|
207
|
+
),
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
return () => {
|
|
211
|
+
unsubscribe()
|
|
212
|
+
unregister()
|
|
213
|
+
clearInterval(summaryClock)
|
|
214
|
+
}
|
|
215
|
+
},
|
|
216
|
+
})
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Shared value types for the crosstalk mesh.
|
|
3
|
+
*
|
|
4
|
+
* Nothing in `src/core` imports an OpenCode package: the host adapter in
|
|
5
|
+
* `src/index.ts` translates OpenCode events and tool calls into these plain
|
|
6
|
+
* shapes, which keeps the coordination logic unit-testable in isolation.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** Coarse liveness of a peer session, derived from the server event stream. */
|
|
10
|
+
export type PeerStatus = "running" | "idle" | "unknown"
|
|
11
|
+
|
|
12
|
+
export type Scope = "project" | "location" | "server"
|
|
13
|
+
|
|
14
|
+
/** What a session told the mesh about itself through `crosstalk_status`. */
|
|
15
|
+
export interface Declared {
|
|
16
|
+
/**
|
|
17
|
+
* A unique human name chosen by the session, so the user can say "tell
|
|
18
|
+
* George…" and peers can address it in `crosstalk_send`. Optional; the
|
|
19
|
+
* session id stays the authoritative identity.
|
|
20
|
+
*/
|
|
21
|
+
name?: string
|
|
22
|
+
role?: string
|
|
23
|
+
goal?: string
|
|
24
|
+
/**
|
|
25
|
+
* What the session is doing *right now*: one or two short sentences, kept
|
|
26
|
+
* current as the work moves. Unlike `goal` — the overall objective, declared
|
|
27
|
+
* once — this is the live status line shown under the avatar in the TUI
|
|
28
|
+
* sidebar and read by peers through `crosstalk_peers`, so it is meant to be
|
|
29
|
+
* re-declared whenever the task changes.
|
|
30
|
+
*/
|
|
31
|
+
summary?: string
|
|
32
|
+
/**
|
|
33
|
+
* When `summary` was last declared, in ms. Refreshing any other declared
|
|
34
|
+
* field leaves it alone, so it ages the *summary* rather than the
|
|
35
|
+
* declaration. Absent for summaries restored from a snapshot written by an
|
|
36
|
+
* older version — those are neither annotated nor dimmed, just shown.
|
|
37
|
+
*/
|
|
38
|
+
summaryAt?: number
|
|
39
|
+
workingOn?: string[]
|
|
40
|
+
note?: string
|
|
41
|
+
/**
|
|
42
|
+
* Optional avatar hint (an emoji such as `👩` or `👨`) that steers which
|
|
43
|
+
* character pool the portrait is drawn from. The concrete artwork is chosen
|
|
44
|
+
* by the mesh on the session's first task-bearing declaration and then
|
|
45
|
+
* frozen on the peer record — see `Peer.portrait`.
|
|
46
|
+
*/
|
|
47
|
+
avatar?: string
|
|
48
|
+
updatedAt?: number
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** One session the mesh knows about. */
|
|
52
|
+
export interface Peer {
|
|
53
|
+
sessionID: string
|
|
54
|
+
title?: string
|
|
55
|
+
agent?: string
|
|
56
|
+
/** Human readable `provider/model#variant` label. */
|
|
57
|
+
model?: string
|
|
58
|
+
parentID?: string
|
|
59
|
+
projectID?: string
|
|
60
|
+
directory?: string
|
|
61
|
+
status: PeerStatus
|
|
62
|
+
created?: number
|
|
63
|
+
/** Timestamp of the most recent event seen for this session. */
|
|
64
|
+
lastSeen: number
|
|
65
|
+
declared?: Declared
|
|
66
|
+
/**
|
|
67
|
+
* Frozen portrait: `pool/file` inside `assets/avatars/`, assigned by the
|
|
68
|
+
* mesh when the session first declares a task and never changed again —
|
|
69
|
+
* re-declaring a different role, name, or avatar hint cannot move it.
|
|
70
|
+
*/
|
|
71
|
+
portrait?: string
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** A peer enriched with mesh-derived state, as returned to tools. */
|
|
75
|
+
export interface PeerView extends Peer {
|
|
76
|
+
isSelf: boolean
|
|
77
|
+
/** No event for longer than the stale threshold. */
|
|
78
|
+
stale: boolean
|
|
79
|
+
unread: number
|
|
80
|
+
claims: string[]
|
|
81
|
+
/**
|
|
82
|
+
* Provably within the configured communication scope. Only addressable
|
|
83
|
+
* peers can be messaged or awaited; the rest are listed for visibility but
|
|
84
|
+
* marked "not addressable".
|
|
85
|
+
*/
|
|
86
|
+
addressable: boolean
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export type MessageKind = "message" | "status" | "request" | "answer" | "system"
|
|
90
|
+
|
|
91
|
+
export type Delivery = "steer" | "queue"
|
|
92
|
+
|
|
93
|
+
/** A message delivered into one recipient's mailbox. */
|
|
94
|
+
export interface CrosstalkMessage {
|
|
95
|
+
id: string
|
|
96
|
+
seq: number
|
|
97
|
+
from: string
|
|
98
|
+
/** The sender's declared human name, stamped at send time. */
|
|
99
|
+
fromName?: string
|
|
100
|
+
fromRole?: string
|
|
101
|
+
to: string
|
|
102
|
+
text: string
|
|
103
|
+
topic?: string
|
|
104
|
+
kind: MessageKind
|
|
105
|
+
delivery: Delivery
|
|
106
|
+
created: number
|
|
107
|
+
readAt?: number
|
|
108
|
+
meta?: Record<string, unknown>
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** An exclusive lease on a path or named resource. */
|
|
112
|
+
export interface Claim {
|
|
113
|
+
/** Normalized comparison key. */
|
|
114
|
+
key: string
|
|
115
|
+
/** The resource string as the holder wrote it. */
|
|
116
|
+
raw: string
|
|
117
|
+
holder: string
|
|
118
|
+
note?: string
|
|
119
|
+
acquired: number
|
|
120
|
+
expires: number
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export interface ClaimConflict {
|
|
124
|
+
key: string
|
|
125
|
+
raw: string
|
|
126
|
+
holder: string
|
|
127
|
+
note?: string
|
|
128
|
+
expires: number
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export interface ClaimOutcome {
|
|
132
|
+
claimed: string[]
|
|
133
|
+
renewed: string[]
|
|
134
|
+
released: string[]
|
|
135
|
+
stolen: string[]
|
|
136
|
+
conflicts: ClaimConflict[]
|
|
137
|
+
/** True when a single conflict aborted the whole request. */
|
|
138
|
+
rolledBack: boolean
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** Serialized mesh state. Messages are intentionally not persisted. */
|
|
142
|
+
export interface MeshSnapshot {
|
|
143
|
+
version: number
|
|
144
|
+
savedAt: number
|
|
145
|
+
peers: Peer[]
|
|
146
|
+
claims: Claim[]
|
|
147
|
+
}
|
package/tui.ts
ADDED