soulnet-dsh 0.1.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.
Files changed (72) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +64 -0
  3. package/cordis.patch.yml +40 -0
  4. package/lib/client.js +5427 -0
  5. package/lib/client.js.map +1 -0
  6. package/lib/commands.js +94 -0
  7. package/lib/commands.js.map +1 -0
  8. package/lib/events-Cz7g9aXF.js +134 -0
  9. package/lib/events-Cz7g9aXF.js.map +1 -0
  10. package/lib/events-DHPK3pow.js +134 -0
  11. package/lib/events-DHPK3pow.js.map +1 -0
  12. package/lib/events-DViXNRAZ.js +9 -0
  13. package/lib/events-DViXNRAZ.js.map +1 -0
  14. package/lib/index.js +2547 -0
  15. package/lib/index.js.map +1 -0
  16. package/lib/policy-HQKLctKr.js +96 -0
  17. package/lib/policy-HQKLctKr.js.map +1 -0
  18. package/lib/send-BL_Yr40O.js +33 -0
  19. package/lib/send-BL_Yr40O.js.map +1 -0
  20. package/lib/send-DcYQC5CC.js +127 -0
  21. package/lib/send-DcYQC5CC.js.map +1 -0
  22. package/lib/sessions.js +1197 -0
  23. package/lib/sessions.js.map +1 -0
  24. package/lib/tools.js +342 -0
  25. package/lib/tools.js.map +1 -0
  26. package/lib/types/alter-state.d.ts +185 -0
  27. package/lib/types/api/index.d.ts +52 -0
  28. package/lib/types/client/A2ANode.d.ts +4 -0
  29. package/lib/types/client/AlterPane.d.ts +9 -0
  30. package/lib/types/client/ChatPane.d.ts +13 -0
  31. package/lib/types/client/DraftCard.d.ts +11 -0
  32. package/lib/types/client/FriendComposer.d.ts +20 -0
  33. package/lib/types/client/FriendList.d.ts +12 -0
  34. package/lib/types/client/FriendPane.d.ts +13 -0
  35. package/lib/types/client/Inbox.d.ts +9 -0
  36. package/lib/types/client/InboxOverlay.d.ts +8 -0
  37. package/lib/types/client/Onboarding.d.ts +4 -0
  38. package/lib/types/client/SettingsSection.d.ts +29 -0
  39. package/lib/types/client/SidebarEntry.d.ts +9 -0
  40. package/lib/types/client/SoulmirrorPage.d.ts +12 -0
  41. package/lib/types/client/SoulmirrorView.d.ts +5 -0
  42. package/lib/types/client/a2a-node.d.ts +46 -0
  43. package/lib/types/client/alter-ui.d.ts +35 -0
  44. package/lib/types/client/api.d.ts +424 -0
  45. package/lib/types/client/inbox-state.d.ts +57 -0
  46. package/lib/types/client/index.d.ts +30 -0
  47. package/lib/types/client/locales.d.ts +239 -0
  48. package/lib/types/client/page-state.d.ts +116 -0
  49. package/lib/types/client/page-store.d.ts +96 -0
  50. package/lib/types/client/panel-store.d.ts +30 -0
  51. package/lib/types/client/styles.d.ts +2 -0
  52. package/lib/types/commands/index.d.ts +24 -0
  53. package/lib/types/drafts.d.ts +50 -0
  54. package/lib/types/events.d.ts +61 -0
  55. package/lib/types/friend-settings.d.ts +41 -0
  56. package/lib/types/index.d.ts +43 -0
  57. package/lib/types/network/fake.d.ts +10 -0
  58. package/lib/types/network/jsonrpc.d.ts +52 -0
  59. package/lib/types/network/send.d.ts +19 -0
  60. package/lib/types/network/soulnet.d.ts +123 -0
  61. package/lib/types/network/types.d.ts +203 -0
  62. package/lib/types/persona.d.ts +45 -0
  63. package/lib/types/policy.d.ts +105 -0
  64. package/lib/types/sessions/index.d.ts +182 -0
  65. package/lib/types/settings.d.ts +58 -0
  66. package/lib/types/tools/define.d.ts +35 -0
  67. package/lib/types/tools/index.d.ts +57 -0
  68. package/lib/types-zwvjR1n5.js +29 -0
  69. package/lib/types-zwvjR1n5.js.map +1 -0
  70. package/package.json +133 -0
  71. package/presets/soulmirror-chat/agent.cordis.yml +24 -0
  72. package/presets/soulmirror-chat/preset.yml +3 -0
@@ -0,0 +1,239 @@
1
+ export declare const NS = "soulmirror";
2
+ export declare const en: {
3
+ readonly 'view.tab': "SoulMirror";
4
+ readonly 'sidebar.entry': "SoulMirror";
5
+ readonly 'sidebar.unread': "{n} unread";
6
+ readonly 'inbox.title': "SoulMirror";
7
+ readonly 'inbox.hint': "Your alter speaks to your friends on your behalf. Open SoulMirror to talk to it, watch its conversations and review its drafts; click a friend to open that thread.";
8
+ readonly 'inbox.friends': "Friends";
9
+ readonly 'inbox.pending': "Pending requests ({n})";
10
+ readonly 'inbox.pending.noGreeting': "(no greeting)";
11
+ readonly 'inbox.pending.accepted': "Accepted {name}.";
12
+ readonly 'inbox.empty': "No friends yet — paste a card URI below, or /add.";
13
+ readonly 'inbox.empty.noIdentity': "No identity yet — create one in Settings → SoulMirror network.";
14
+ readonly 'inbox.noIdentity': "No identity yet";
15
+ readonly 'inbox.noMessages': "No messages yet";
16
+ readonly 'inbox.typing': "typing…";
17
+ readonly 'inbox.online': "online";
18
+ readonly 'inbox.offline': "offline";
19
+ readonly 'inbox.open': "Open the thread with {name}";
20
+ readonly 'inbox.close': "Close";
21
+ readonly 'inbox.card.copy': "Copy my card URI";
22
+ readonly 'inbox.card.copied': "Copied";
23
+ readonly 'inbox.add': "Add";
24
+ readonly 'inbox.add.placeholder': "Add a friend: paste a card URI (soulmirror://card?…)";
25
+ readonly 'toast.newMail': "New message from {name}";
26
+ readonly 'bubble.in': "from {name}";
27
+ readonly 'bubble.out': "to {name}";
28
+ readonly 'bubble.mine': "my alter";
29
+ readonly 'bubble.auto': "auto-reply";
30
+ readonly 'bubble.note': "SoulMirror";
31
+ readonly 'settings.nav': "SoulMirror network";
32
+ readonly 'settings.title': "SoulMirror network";
33
+ readonly 'settings.intro': "This dsh is a node on the SoulMirror network: an end-to-end encrypted A2A identity, friends, and your alter — one dsh session (\"My alter\") that speaks to every friend on your behalf.";
34
+ readonly 'settings.status': "Backend";
35
+ readonly 'settings.status.soulnet': "soulnet light peer";
36
+ readonly 'settings.status.fake': "in-memory fake";
37
+ readonly 'settings.state.ready': "running";
38
+ readonly 'settings.state.starting': "starting…";
39
+ readonly 'settings.state.restarting': "restarting…";
40
+ readonly 'settings.state.stopped': "stopped";
41
+ readonly 'settings.state.error': "error";
42
+ readonly 'settings.binary': "binary";
43
+ readonly 'settings.binary.setting': "from the peerBinary setting";
44
+ readonly 'settings.binary.platform-package': "from the platform package";
45
+ readonly 'settings.binary.path': "from PATH";
46
+ readonly 'settings.binary.plugin-bin': "from the plugin bin/ directory";
47
+ readonly 'settings.identity': "Identity";
48
+ readonly 'settings.identity.none': "No identity yet. Pick a display name to create one — this generates your key pair locally.";
49
+ readonly 'settings.identity.name': "Display name";
50
+ readonly 'settings.identity.create': "Create identity";
51
+ readonly 'settings.identity.creating': "Creating…";
52
+ readonly 'settings.card': "My card URI";
53
+ readonly 'settings.card.copy': "Copy";
54
+ readonly 'settings.card.copied': "Copied";
55
+ readonly 'settings.card.hint': "Send this link to people who should add you; they paste it in their SoulMirror (or /add here).";
56
+ readonly 'settings.backup': "Back up identity.json";
57
+ readonly 'settings.backup.body': "Your identity is the private key in {path}. Losing it means losing this identity, friends and history — copy the whole a2a/ directory somewhere safe. Moving to SoulMirror later = copying it to ~/.soulmirror/a2a/.";
58
+ readonly 'settings.config': "Connection";
59
+ readonly 'settings.config.relay': "Relay URL";
60
+ readonly 'settings.config.displayName': "Display name (for a new identity)";
61
+ readonly 'settings.config.backend': "Backend";
62
+ readonly 'settings.config.peerBinary': "soulnet binary path";
63
+ readonly 'settings.config.home': "Data directory";
64
+ readonly 'settings.config.hint': "Changes apply the next time dsh starts (the peer process is spawned with them). Empty path = PATH lookup, then the plugin's bin/; empty directory = $SOULNET_HOME, then ~/.soulnet.";
65
+ readonly 'settings.config.unavailable': "Settings are editable from a local (loopback) browser only.";
66
+ readonly 'settings.friends': "Friends";
67
+ readonly 'settings.friends.hint': "Friends are read-only threads on the SoulMirror page: your alter writes to them, you tell your alter what to say.";
68
+ readonly 'settings.friends.none': "No friends yet. Paste a card URI below or use /add.";
69
+ readonly 'settings.friends.open': "Open on the page";
70
+ readonly 'settings.friends.online': "online";
71
+ readonly 'settings.friends.offline': "offline";
72
+ readonly 'settings.friends.unread': "{n} unread";
73
+ readonly 'settings.friends.typing': "typing…";
74
+ readonly 'settings.pending': "Pending requests";
75
+ readonly 'settings.pending.none': "No pending requests.";
76
+ readonly 'settings.pending.accept': "Accept";
77
+ readonly 'settings.pending.reject': "Reject";
78
+ readonly 'settings.add': "Add a friend";
79
+ readonly 'settings.add.uri': "Card URI (soulmirror://card?…)";
80
+ readonly 'settings.add.note': "Note / name (optional, also the greeting)";
81
+ readonly 'settings.add.send': "Send request";
82
+ readonly 'settings.add.sent': "Request sent to {name}. The thread appears once they accept.";
83
+ readonly 'settings.error': "Error: {message}";
84
+ readonly 'settings.refresh': "Refresh";
85
+ readonly 'onboard.title': "Welcome to the SoulMirror network";
86
+ readonly 'onboard.body': "Create your A2A identity: a key pair generated on this machine. Others add you by pasting your card URI.";
87
+ readonly 'onboard.name': "Display name";
88
+ readonly 'onboard.create': "Create identity";
89
+ readonly 'onboard.done': "Done";
90
+ readonly 'onboard.later': "Later";
91
+ readonly 'onboard.created': "Identity created. Copy your card URI and back up identity.json.";
92
+ readonly 'onboard.waiting': "The soulnet peer is not running yet ({state}). Check Settings → SoulMirror network.";
93
+ readonly 'cmd.friends.open': "Open the thread with {name}";
94
+ readonly 'cmd.friends.none': "No friends yet";
95
+ readonly 'cmd.soulmirror.page': "Open SoulMirror — My alter";
96
+ readonly 'cmd.soulmirror.page.detail': "Talk to your alter; friends on the left are its conversations";
97
+ readonly 'page.title': "SoulMirror";
98
+ readonly 'page.close': "Back to dsh";
99
+ readonly 'page.search': "Search friends";
100
+ readonly 'page.pending': "New friends";
101
+ readonly 'page.friends': "Friends";
102
+ readonly 'page.identity.pending': "{n} pending";
103
+ readonly 'page.drafts.count': "{n} to review";
104
+ readonly 'page.row.draftTag': "[draft]";
105
+ readonly 'page.add': "Add friend";
106
+ readonly 'page.add.uri': "Card URI (soulmirror://card?…)";
107
+ readonly 'page.add.note': "Note (optional, also the greeting)";
108
+ readonly 'page.add.send': "Send request";
109
+ readonly 'page.empty.noFriends': "No friends yet. Paste a card URI under \"Add friend\" at the bottom of the list, or send yours to a friend.";
110
+ readonly 'page.empty.noFriends.noIdentity': "No identity yet — create one in Settings → SoulMirror network.";
111
+ readonly 'page.empty.noMatch': "No friend matches \"{query}\".";
112
+ readonly 'page.header.online': "online";
113
+ readonly 'page.header.offline': "offline";
114
+ readonly 'page.header.lastSeen': "last message {age} ago";
115
+ readonly 'page.header.lastSeen.now': "last message just now";
116
+ readonly 'page.header.card': "Card";
117
+ readonly 'page.header.card.title': "Card of {name}";
118
+ readonly 'page.header.card.copy': "Copy";
119
+ readonly 'page.header.card.copied': "Copied";
120
+ readonly 'page.header.markRead': "Mark read";
121
+ readonly 'page.thread.loading': "Loading…";
122
+ readonly 'page.thread.loadOlder': "Load older messages";
123
+ readonly 'page.thread.start': "Start of the conversation";
124
+ readonly 'page.thread.today': "Today";
125
+ readonly 'page.thread.yesterday': "Yesterday";
126
+ readonly 'page.thread.typing': "{name} is typing…";
127
+ readonly 'page.thread.attachment': "attachment: {name}";
128
+ readonly 'page.status.sending': "sending…";
129
+ readonly 'page.status.sent': "sent";
130
+ readonly 'page.status.queued': "queued — relay unreachable, will retry";
131
+ readonly 'page.status.error': "not delivered";
132
+ readonly 'page.status.failed': "failed: {message}";
133
+ readonly 'page.status.retry': "Retry";
134
+ readonly 'page.status.discard': "Discard";
135
+ readonly 'page.composer.send': "Send";
136
+ readonly 'page.composer.instructing': "Sending…";
137
+ readonly 'page.composer.direct': "Debug: direct to {name} · end-to-end encrypted · your alter does not see it";
138
+ readonly 'page.open': "Open SoulMirror";
139
+ readonly 'alter.me': "My alter";
140
+ readonly 'alter.row.hint': "Talk to your alter directly";
141
+ readonly 'alter.row.drafts': "{n} draft(s) waiting for you";
142
+ readonly 'alter.status.running': "Your alter is working…";
143
+ readonly 'alter.status.idle': "Ready · one memory shared across every friend";
144
+ readonly 'alter.status.noSession': "Starting the alter session…";
145
+ readonly 'alter.openDsh': "Open in dsh";
146
+ readonly 'alter.openDsh.hint': "The same conversation as a native dsh session (trajectory, tools, approvals).";
147
+ readonly 'alter.pendbar': "{n} draft(s) of your alter wait for your decision";
148
+ readonly 'alter.pendbar.go': "Review in {name}'s thread →";
149
+ readonly 'alter.empty.title': "Tell your alter what to do";
150
+ readonly 'alter.empty.hint': "It writes to your friends on your behalf (\"tell Bob I am in\"), answers their mail by your diplomacy protocol, and asks you before money, tasks or commitments. Friends on the left are its conversations — read-only for you.";
151
+ readonly 'alter.composer.placeholder': "Say something to your alter… (Enter to send, Shift+Enter for a new line)";
152
+ readonly 'alter.composer.hint': "You only talk to your own alter; it speaks to your friends for you.";
153
+ readonly 'alter.proactive': "proactive";
154
+ readonly 'alter.thinking': "Your alter is working…";
155
+ readonly 'alter.item.inbound': "Mail from {name}";
156
+ readonly 'alter.item.view': "open thread";
157
+ readonly 'alter.item.send': "To {name}";
158
+ readonly 'alter.item.send.pending': "sending…";
159
+ readonly 'alter.item.send.sent': "sent";
160
+ readonly 'alter.item.send.auto': "sent · auto";
161
+ readonly 'alter.item.send.draft': "draft — waiting for your review";
162
+ readonly 'alter.item.send.refused': "not sent";
163
+ readonly 'alter.item.send.failed': "failed";
164
+ readonly 'alter.item.note.approved': "You approved the draft to {name}; it was sent";
165
+ readonly 'alter.item.note.rejected': "You rejected the draft to {name}";
166
+ readonly 'alter.item.note.revise': "You sent the draft to {name} back for revision";
167
+ readonly 'alter.item.turnFailed': "The turn failed: {reason}";
168
+ readonly 'alter.item.revise': "Revision for {name}";
169
+ readonly 'alter.failed.hint': "No model answer — check the model / API key in dsh, then try again.";
170
+ readonly 'draft.tag': "Draft";
171
+ readonly 'draft.title': "Your alter wants to send this";
172
+ readonly 'draft.title.to': "Your alter wants to send this to {name}";
173
+ readonly 'draft.openThread': "open thread";
174
+ readonly 'draft.reason.draft-tier': "Held because this friend's reply tier is \"draft\" (you review before anything goes out).";
175
+ readonly 'draft.reason.notify-tier': "Held because this friend's reply tier is \"notify\".";
176
+ readonly 'draft.reason.rate-limited': "Held because the hourly auto-reply cap for this friend is reached.";
177
+ readonly 'draft.reason.loop-guard-auto': "Held because the mail that woke your alter was itself an automatic reply (loop guard).";
178
+ readonly 'draft.reason.other-friend': "Held because your alter wants to write to a different friend than the one who wrote.";
179
+ readonly 'draft.reason.unknown-trigger': "Held because your alter was not acting on your instruction or on this friend's mail.";
180
+ readonly 'draft.inReplyTo': "in reply to mail from {name}";
181
+ readonly 'draft.approve': "Send it";
182
+ readonly 'draft.edit': "Edit";
183
+ readonly 'draft.editSend': "Send edited";
184
+ readonly 'draft.revise': "Let the alter revise";
185
+ readonly 'draft.revise.placeholder': "Tell your alter what to change (it rewrites and sends on your word)…";
186
+ readonly 'draft.reviseSend': "Send feedback to the alter";
187
+ readonly 'draft.reject': "Reject";
188
+ readonly 'draft.cancel': "Cancel";
189
+ readonly 'friend.banner.prefix': "This is ";
190
+ readonly 'friend.banner.mine': "your alter";
191
+ readonly 'friend.banner.middle': "'s conversation with ";
192
+ readonly 'friend.banner.theirs': "{name}";
193
+ readonly 'friend.banner.suffix': " — you can't speak here. Tell \"My alter\" instead.";
194
+ readonly 'friend.banner.go': "Go to My alter";
195
+ readonly 'friend.empty': "No messages yet — tell \"My alter\" to write to {name}.";
196
+ readonly 'friend.drafts.pill': "{n} draft(s) waiting for your decision";
197
+ readonly 'friend.actbar.eye': "Alter ↔ alter · you are the observer";
198
+ readonly 'friend.actbar.pending': "Drafts";
199
+ readonly 'friend.actbar.settings': "Protocol & settings";
200
+ readonly 'friend.actbar.goAlter': "Talk to My alter →";
201
+ readonly 'friend.debug.placeholder': "Debug: send as myself to {name}… (bypasses the alter)";
202
+ readonly 'friend.debug.send': "Send as myself";
203
+ readonly 'tier.notify': "Notify only";
204
+ readonly 'tier.draft': "Draft — you review before it sends";
205
+ readonly 'tier.auto': "Auto-reply — no review (rate-limited)";
206
+ readonly 'tier.default': "Default ({tier})";
207
+ readonly 'tier.short.notify': "notify";
208
+ readonly 'tier.short.draft': "draft";
209
+ readonly 'tier.short.auto': "auto";
210
+ readonly 'friend.settings.title': "How your alter handles {name}";
211
+ readonly 'friend.tier': "Reply tier";
212
+ readonly 'friend.tier.hint': "notify: mail is only shown; draft: your alter drafts a reply and you review it on this page; auto: it sends by itself (max {n}/hour).";
213
+ readonly 'friend.protocol': "Protocol override for this friend";
214
+ readonly 'friend.protocol.placeholder': "Rules for this friend only (they win over the global protocol on conflict). Empty = none.";
215
+ readonly 'friend.protocol.save': "Save";
216
+ readonly 'friend.protocol.saved': "Saved.";
217
+ readonly 'protocol.title': "Diplomacy protocol";
218
+ readonly 'protocol.open': "Protocol";
219
+ readonly 'protocol.hint': "The principles your alter follows when it speaks to friends on your behalf (global; per-friend overrides live under \"Protocol & settings\" in a friend thread). Saved to {path}; applies from the next turn.";
220
+ readonly 'protocol.missing': "No protocol.md yet — the peer writes the default when your identity is created; saving here creates it.";
221
+ readonly 'protocol.save': "Save protocol";
222
+ readonly 'protocol.saved': "Saved.";
223
+ readonly 'protocol.reload': "Reload";
224
+ readonly 'settings.alter': "Your alter";
225
+ readonly 'settings.alter.intro': "One dsh session (\"My alter\") is your alter: you tell it what to say, it writes to your friends and answers their mail by the diplomacy protocol. In the \"draft\" tier its replies wait for your review on the SoulMirror page; \"auto\" sends by itself (rate-limited); \"notify\" only shows the mail. These settings apply immediately.";
226
+ readonly 'settings.alter.defaultTier': "Default reply tier";
227
+ readonly 'settings.alter.perHour': "Auto replies per friend per hour";
228
+ readonly 'settings.alter.directSend': "Debug: offer \"Send as myself\" in friend threads (bypasses the alter)";
229
+ readonly 'settings.alter.openPage': "Open SoulMirror";
230
+ readonly 'settings.alter.openSession': "Open the alter session in dsh";
231
+ readonly 'settings.alter.legacy': "{n} old per-friend session(s) from before 0.1.0-p4 are kept in dsh but receive no mail; you can delete them.";
232
+ };
233
+ export declare const zh: Record<keyof typeof en, string>;
234
+ export type SoulmirrorKey = keyof typeof en;
235
+ declare module '@deepseek-ai/dsh-client-ui-slots' {
236
+ interface LocaleNamespaceMap {
237
+ soulmirror: SoulmirrorKey;
238
+ }
239
+ }
@@ -0,0 +1,116 @@
1
+ /**
2
+ * Pure state of the SoulMirror page (P2b): which friend is selected, the
3
+ * per-friend message thread (archive entries + optimistic outbound bubbles),
4
+ * and the row / bubble helpers. Folds: `conversation.get` answers (merge by
5
+ * seq, dedupe by id), SSE `message` frames (inbound), SSE `outbound` frames
6
+ * (another tab sent), optimistic send → reconcile / fail. No React, no DOM:
7
+ * unit-tested under node (test/page-state.test.ts). The friend list itself
8
+ * is the inbox state (./inbox-state.ts); this module only adds the search
9
+ * filter and the selection rule on top of it.
10
+ */
11
+ import type { ApiEntry } from './api.ts';
12
+ import { type InboxFriend } from './inbox-state.ts';
13
+ /** One bubble of the thread: an archived entry, or an outbound one not yet acknowledged by the peer. */
14
+ export interface ThreadEntry {
15
+ /** Archive line number (1-based); 0 while the entry is only optimistic. */
16
+ readonly seq: number;
17
+ readonly dir: 'in' | 'out';
18
+ /** A2A message id; for optimistic entries the client id until the receipt arrives. */
19
+ readonly id: string;
20
+ readonly body: string;
21
+ /** Unix epoch ms. */
22
+ readonly ts: number;
23
+ readonly type?: string;
24
+ readonly auto?: true;
25
+ /** Outbound delivery state: `sending` (optimistic) | `sent` | `queued` | `error` | `failed` (the send call itself failed). */
26
+ readonly status?: string;
27
+ readonly artifactName?: string;
28
+ /** Client-side id of an optimistic send (stable across reconcile; the React key). */
29
+ readonly clientId?: string;
30
+ /** Error text when the send call failed (status `failed`). */
31
+ readonly error?: string;
32
+ }
33
+ export interface ThreadState {
34
+ readonly entries: readonly ThreadEntry[];
35
+ /** The `limit` of the last archive fetch (grows when the owner scrolls up). */
36
+ readonly window: number;
37
+ /** The archive has no older entries than the ones loaded. */
38
+ readonly complete: boolean;
39
+ readonly loading: boolean;
40
+ readonly loaded: boolean;
41
+ readonly error?: string;
42
+ }
43
+ export declare const EMPTY_THREAD: ThreadState;
44
+ /** Entries per archive fetch; the window grows by this much per "load older". */
45
+ export declare const PAGE_SIZE = 50;
46
+ export declare function entryFromApi(entry: ApiEntry): ThreadEntry;
47
+ /** Stable ordering: archived entries by seq, then optimistic ones (seq 0) by time at the tail. */
48
+ export declare function sortThread(entries: readonly ThreadEntry[]): ThreadEntry[];
49
+ /**
50
+ * Merge archive entries into the thread: an entry with the same seq replaces
51
+ * the loaded one (status may have moved from queued → sent), an entry with
52
+ * the same id as an optimistic bubble absorbs it (keeps its clientId so the
53
+ * React key is stable), everything else is appended. Result sorted.
54
+ */
55
+ export declare function mergeEntries(existing: readonly ThreadEntry[], incoming: readonly ThreadEntry[]): ThreadEntry[];
56
+ /** Fold one `conversation.get` answer (the last `window` entries) into the thread. */
57
+ export declare function applyArchive(state: ThreadState, entries: readonly ApiEntry[], window: number): ThreadState;
58
+ /** Append one inbound message (SSE `message` frame) to the thread. */
59
+ export declare function applyInbound(state: ThreadState, message: {
60
+ id: string;
61
+ body: string;
62
+ ts: number;
63
+ seq?: number;
64
+ type?: string;
65
+ auto?: true;
66
+ artifactName?: string;
67
+ }): ThreadState;
68
+ /** Append one archived outbound entry (SSE `outbound` frame from another tab, or our own receipt). */
69
+ export declare function applyOutbound(state: ThreadState, entry: ApiEntry): ThreadState;
70
+ /** The owner pressed Send: show the bubble at once (status `sending`). */
71
+ export declare function addOptimistic(state: ThreadState, draft: {
72
+ clientId: string;
73
+ body: string;
74
+ ts: number;
75
+ }): ThreadState;
76
+ /** The host answered `message.send` with the archived entry: replace the optimistic bubble in place. */
77
+ export declare function reconcileSend(state: ThreadState, clientId: string, entry: ApiEntry): ThreadState;
78
+ /** The send call failed: keep the bubble, mark it `failed` with the error. */
79
+ export declare function failSend(state: ThreadState, clientId: string, error: string): ThreadState;
80
+ /** Remove a failed optimistic bubble (the owner discarded it or retries). */
81
+ export declare function dropEntry(state: ThreadState, clientId: string): ThreadState;
82
+ /** Highest archived seq in the thread (the read cursor to report). */
83
+ export declare function lastSeq(state: ThreadState): number;
84
+ /** Case-insensitive filter over name / remark / card name / fingerprint prefix. */
85
+ export declare function filterFriends(friends: readonly InboxFriend[], query: string): InboxFriend[];
86
+ /** Rows of the middle column: unread first, then newest, then by name; then the search filter. */
87
+ export declare function listRows(friends: readonly InboxFriend[], query: string): InboxFriend[];
88
+ /** Selection key of the pinned first item "My alter" (never a fingerprint). */
89
+ export declare const ALTER_KEY = "alter";
90
+ /**
91
+ * Which item the page shows on the right: "My alter" (the pinned first item,
92
+ * also the default — selecting it has no side effect) or the explicitly
93
+ * selected friend while it is still a friend (selecting a friend marks it
94
+ * read, so a gone friend never re-selects itself; it falls back to the alter).
95
+ */
96
+ export declare function resolveSelection(friends: readonly Pick<InboxFriend, 'fp'>[], selected: string | undefined): string;
97
+ /** Local calendar day key (YYYY-MM-DD) for the day separators. */
98
+ export declare function dayKey(ts: number): string;
99
+ export type ThreadRow = {
100
+ readonly kind: 'day';
101
+ readonly key: string;
102
+ readonly ts: number;
103
+ } | {
104
+ readonly kind: 'entry';
105
+ readonly key: string;
106
+ readonly entry: ThreadEntry;
107
+ };
108
+ /** Interleave day separators: one before the first entry of every local day. */
109
+ export declare function withDaySeparators(entries: readonly ThreadEntry[]): ThreadRow[];
110
+ /** Bubble time: HH:MM (24 h, local). */
111
+ export declare function formatClock(ts: number): string;
112
+ /** Day separator label: "Today" / "Yesterday" (via the dictionary) or a local date. */
113
+ export declare function formatDay(ts: number, now: number, labels: {
114
+ today: string;
115
+ yesterday: string;
116
+ }): string;
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Open/closed state and selection of the SoulMirror page plus the per-friend
3
+ * thread cache and the "My alter" transcript, shared between the sidebar
4
+ * footer button (`sidebar.footer.action`), the `/soulmirror` popup, the view
5
+ * tab and the page itself (`shell.overlay`). Threads are folded with the pure
6
+ * functions of ./page-state.ts: archive fetches (`conversation.get`), SSE
7
+ * `message` / `outbound` frames (through `networkStore.onFrame`), optimistic
8
+ * debug sends and their reconcile. The alter transcript is fetched from
9
+ * `session.history` and refetched (debounced) on every SSE `alter` frame.
10
+ * One store per page; components read it through `useSyncExternalStore`.
11
+ */
12
+ import { type ApiChat } from './api.ts';
13
+ import { type ThreadState } from './page-state.ts';
14
+ /** The alter transcript as the page holds it (P4). */
15
+ export interface AlterView {
16
+ readonly sessionId: string | undefined;
17
+ readonly status: 'idle' | 'running';
18
+ readonly chat: ApiChat;
19
+ readonly loading: boolean;
20
+ readonly loaded: boolean;
21
+ /** An instruction is being posted. */
22
+ readonly instructing: boolean;
23
+ readonly error?: string;
24
+ }
25
+ export declare const EMPTY_ALTER: AlterView;
26
+ export interface PageSnapshot {
27
+ readonly open: boolean;
28
+ /** Selection: `ALTER_KEY` or a friend fingerprint (may point at a friend that is gone; the page resolves it). */
29
+ readonly selected: string | undefined;
30
+ /** fp → thread (only for friends whose archive was fetched at least once). */
31
+ readonly threads: Readonly<Record<string, ThreadState>>;
32
+ readonly alter: AlterView;
33
+ /** Draft ids with a decision in flight. */
34
+ readonly deciding: readonly string[];
35
+ }
36
+ /** Items of the transcript fetched per load. */
37
+ export declare const HISTORY_LIMIT = 200;
38
+ export declare class PageStore {
39
+ private snapshot;
40
+ private readonly listeners;
41
+ private readonly typingSentAt;
42
+ private readonly typingIdle;
43
+ private historyTimer;
44
+ private historyAgain;
45
+ private seq;
46
+ constructor();
47
+ getSnapshot: () => PageSnapshot;
48
+ subscribe: (listener: () => void) => (() => void);
49
+ private set;
50
+ private setThread;
51
+ thread: (fp: string) => ThreadState;
52
+ private setAlter;
53
+ /** Fetch the alter transcript (`session.history`). */
54
+ loadAlter: () => Promise<void>;
55
+ private scheduleHistory;
56
+ /**
57
+ * The owner instructs their alter: the text becomes an owner user/message
58
+ * in the alter session and wakes a turn; the alter decides what to tell
59
+ * which friend and sends it with soulmirror_send_message. The transcript
60
+ * refetches as the session moves (SSE `alter`), the friend thread shows
61
+ * the alter's send as an outbound bubble (SSE `outbound`).
62
+ */
63
+ instruct: (text: string) => Promise<boolean>;
64
+ /** Decide a pending draft (approve / approve edited / reject / revise). */
65
+ decideDraft: (id: string, decision: {
66
+ action: "approve";
67
+ body?: string;
68
+ } | {
69
+ action: "reject";
70
+ } | {
71
+ action: "revise";
72
+ feedback: string;
73
+ }) => Promise<boolean>;
74
+ open: (selection?: string) => void;
75
+ close: () => void;
76
+ toggle: () => void;
77
+ select: (selection: string) => void;
78
+ /** Fetch what the selection needs (the transcript for the alter, the archive for a friend). */
79
+ private prime;
80
+ /** Fetch (or re-fetch) the last `window` entries of a friend's archive. */
81
+ load: (fp: string, window?: number) => Promise<void>;
82
+ /** The owner scrolled to the top: widen the window by one page. */
83
+ loadOlder: (fp: string) => Promise<void>;
84
+ /**
85
+ * Debug direct send (Settings → "send as myself"): optimistic bubble now,
86
+ * reconcile with the archived entry the host answers, or mark it failed.
87
+ */
88
+ send: (fp: string, body: string) => Promise<boolean>;
89
+ /** Discard a failed bubble (returns its body so the composer can offer it again). */
90
+ discard: (fp: string, clientId: string) => string | undefined;
91
+ /** The owner is typing in the debug composer: best-effort `message.typing on`, throttled; `off` after idle. */
92
+ noteTyping: (fp: string) => void;
93
+ stopTyping: (fp: string) => void;
94
+ private onFrame;
95
+ }
96
+ export declare const pageStore: PageStore;
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Open/closed state of the inbox panel, shared between the sidebar footer
3
+ * button (`sidebar.footer.action`, inside the sidebar column) and the panel
4
+ * itself (`shell.overlay`, a root overlay layer). One store per page;
5
+ * components read it through `useSyncExternalStore`.
6
+ */
7
+ export interface PanelAnchor {
8
+ readonly left: number;
9
+ readonly right: number;
10
+ readonly top: number;
11
+ readonly bottom: number;
12
+ }
13
+ export interface PanelSnapshot {
14
+ readonly open: boolean;
15
+ /** Viewport rect of the footer button at open time (the panel sits beside it). */
16
+ readonly anchor: PanelAnchor | undefined;
17
+ }
18
+ export declare class PanelStore {
19
+ private snapshot;
20
+ private readonly listeners;
21
+ getSnapshot: () => PanelSnapshot;
22
+ subscribe: (listener: () => void) => (() => void);
23
+ private set;
24
+ open: (anchor?: PanelAnchor) => void;
25
+ close: () => void;
26
+ toggle: (anchor?: PanelAnchor) => void;
27
+ /** Re-measure (sidebar resized / collapsed while open). */
28
+ anchorTo: (anchor: PanelAnchor) => void;
29
+ }
30
+ export declare const panelStore: PanelStore;
@@ -0,0 +1,2 @@
1
+ export declare function ensureStyles(): void;
2
+ export declare function removeStyles(): void;
@@ -0,0 +1,24 @@
1
+ /**
2
+ * soulmirror-commands — host slash commands (`ctx.commands.register`):
3
+ *
4
+ * /card show this user's card URI
5
+ * /friends list friends (presence, unread); the client half
6
+ * decorates the bare invocation with a popup that opens
7
+ * a friend's session
8
+ * /add <card_uri> [note]
9
+ * send a friend request (the human typing the command
10
+ * IS the approval; commands run outside a model turn, so
11
+ * ctx.approval cannot be used here)
12
+ * /soulmirror open the SoulMirror page ("My alter" + friends); the
13
+ * client half decorates the bare invocation with a
14
+ * popup (the page / one friend) — the host handler only
15
+ * prints the hint, since a host command cannot drive
16
+ * the browser UI
17
+ *
18
+ * Results are rendered by the composer and never enter model history.
19
+ * No @deepseek-ai value imports (see ../index.ts header).
20
+ */
21
+ import type { Context } from '@deepseek-ai/cordis';
22
+ export declare const name = "soulmirror-commands";
23
+ export declare const inject: string[];
24
+ export declare function apply(ctx: Context): void;
@@ -0,0 +1,50 @@
1
+ import type { SendGateReason } from './policy.ts';
2
+ export declare const PENDING_FILE = "dsh-pending.json";
3
+ export interface PendingDraft {
4
+ readonly id: string;
5
+ /** Target friend. */
6
+ readonly fp: string;
7
+ /** Target friend's display name when the draft was stored. */
8
+ readonly name: string;
9
+ readonly body: string;
10
+ /** ISO timestamp. */
11
+ readonly createdAt: string;
12
+ /** Why it became a draft (the send gate's reason). */
13
+ readonly reason: SendGateReason;
14
+ /** What woke the turn that produced it (for the card's "in reply to …" line). */
15
+ readonly trigger?: {
16
+ readonly kind: string;
17
+ readonly fp?: string;
18
+ readonly name?: string;
19
+ readonly messageId?: string;
20
+ };
21
+ /** The alter session that produced it. */
22
+ readonly sessionId?: string;
23
+ }
24
+ /** In-memory copy of dsh-pending.json with write-through. */
25
+ export declare class DraftStore {
26
+ readonly path: string;
27
+ private data;
28
+ private loaded;
29
+ private writing;
30
+ constructor(path: string);
31
+ static at(a2aDir: string): DraftStore;
32
+ load(): Promise<void>;
33
+ get isLoaded(): boolean;
34
+ /** Pending drafts, oldest first; `fp` narrows to one friend. */
35
+ list(fp?: string): readonly PendingDraft[];
36
+ get(id: string): PendingDraft | undefined;
37
+ count(fp?: string): number;
38
+ /** Pending count per friend fingerprint. */
39
+ counts(): Record<string, number>;
40
+ /** Store a new draft (id generated when absent); answers the stored record. */
41
+ add(draft: Omit<PendingDraft, 'id' | 'createdAt'> & {
42
+ id?: string;
43
+ createdAt?: string;
44
+ }): Promise<PendingDraft>;
45
+ /** Remove one draft; answers it (undefined when unknown). */
46
+ remove(id: string): Promise<PendingDraft | undefined>;
47
+ /** Drop every draft of a friend (e.g. the friend was removed). */
48
+ removeAll(fp: string): Promise<number>;
49
+ private flush;
50
+ }
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Shared vocabulary of soulnet-dsh: branded ids and the
3
+ * `user/message` source tag that marks an inbound A2A mail.
4
+ *
5
+ * There is deliberately NO custom SessionEventMap merge here any more. The P0
6
+ * spike (dsh/SPIKE.md §1) showed that a custom durable event type makes the
7
+ * session log unloadable (the persistence read path refuses unknown,
8
+ * non-ignorable types), so every inbound mail is written as an ordinary
9
+ * `user/message` whose `source` carries the fields below; the client half
10
+ * recognises that source and renders the message as a chat bubble.
11
+ *
12
+ * P4: the same relay source also carries the plugin's own NOTES into the
13
+ * alter session (the owner approved / rejected / edited a draft) — marked by
14
+ * `a2a.note`, never woken as a turn, rendered as a system line.
15
+ *
16
+ * Keep this file free of runtime imports apart from constants: the client
17
+ * bundle imports it as well.
18
+ */
19
+ import type { Branded } from '@deepseek-ai/dsh-brand';
20
+ /** Stable A2A message id (the relay envelope id; replay-safe). */
21
+ export type A2AMessageId = Branded<'A2AMessageId'>;
22
+ /** Fingerprint of a peer's signing key (the A2A identity). */
23
+ export type Fingerprint = Branded<'Fingerprint'>;
24
+ /** `source.plugin` of the `user/message` carrying an inbound A2A mail. */
25
+ export declare const SOULMIRROR_PLUGIN = "soulmirror";
26
+ /** `source.form` of that message: dsh's built-in "a message another agent addressed to this one". */
27
+ export declare const RELAY_FORM = "relay";
28
+ /** Kinds of plugin notes written into the alter session (P4 draft decisions). */
29
+ export type A2ANoteKind = 'draft-approved' | 'draft-rejected' | 'draft-revise';
30
+ /** The extra `a2a` block the host attaches to the relay source (wire-extensible; the literal type is closed upstream). */
31
+ export interface A2ASourceMeta {
32
+ readonly id: string;
33
+ readonly fp: string;
34
+ readonly ts: number;
35
+ readonly auto?: true;
36
+ /** A2A message type (`text`, `app_share`, …); absent means `text`. */
37
+ readonly type?: string;
38
+ /** Set on the plugin's own notes (draft decisions): not mail, never a turn trigger. */
39
+ readonly note?: A2ANoteKind;
40
+ /** Draft id the note refers to. */
41
+ readonly draftId?: string;
42
+ }
43
+ /** Shape the client renderer derives from such a `user/message` (one bubble). */
44
+ export interface A2AMessageData {
45
+ readonly id: A2AMessageId;
46
+ /** `in`: friend → me; `out`: me → friend. */
47
+ readonly dir: 'in' | 'out';
48
+ /** Peer fingerprint (the friend this message belongs to). */
49
+ readonly fp: Fingerprint;
50
+ /** Display name of the peer at send time. */
51
+ readonly name: string;
52
+ readonly body: string;
53
+ /** Unix epoch ms at the sender. */
54
+ readonly ts: number;
55
+ /** Outbound delivery state; absent on inbound. */
56
+ readonly delivery?: 'queued' | 'sent' | 'failed';
57
+ /** Set when the message was produced by a peer's auto-reply (loop guard). */
58
+ readonly auto?: true;
59
+ /** Set when the row is a plugin note (draft decision), not mail. */
60
+ readonly note?: A2ANoteKind;
61
+ }