my-durable 0.0.0-stage → 0.0.2

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 (259) hide show
  1. package/.env.example +7 -0
  2. package/LICENSE +21 -0
  3. package/README.md +328 -2
  4. package/ai_docs/durable_pi/announcement.md +860 -0
  5. package/ai_docs/durable_pi/changelog.md +51 -0
  6. package/ai_docs/durable_pi/chord-delta-findings.md +362 -0
  7. package/ai_docs/durable_pi/coding-agent-integration.md +70 -0
  8. package/ai_docs/durable_pi/pico-v5-chord-usage.md +391 -0
  9. package/ai_docs/durable_pi/pico-v5-handoff.md +921 -0
  10. package/ai_docs/durable_pi/readme.md +617 -0
  11. package/ai_docs/durable_pi/sources.md +22 -0
  12. package/ai_docs/durable_pi/spec.md +4696 -0
  13. package/ai_docs/durable_pi/vacation-planner.md +57 -0
  14. package/ai_docs/pi/chord.md +268 -0
  15. package/ai_docs/pi/cli-integration.md +110 -0
  16. package/ai_docs/pi/cli.md +325 -0
  17. package/ai_docs/pi/codemode.md +211 -0
  18. package/ai_docs/pi/compaction.md +467 -0
  19. package/ai_docs/pi/configuration.md +51 -0
  20. package/ai_docs/pi/containerization.md +187 -0
  21. package/ai_docs/pi/custom-provider.md +177 -0
  22. package/ai_docs/pi/environment-variables.md +102 -0
  23. package/ai_docs/pi/extensions.md +278 -0
  24. package/ai_docs/pi/how-pi-works.md +53 -0
  25. package/ai_docs/pi/index.md +43 -0
  26. package/ai_docs/pi/json.md +230 -0
  27. package/ai_docs/pi/keybindings.md +198 -0
  28. package/ai_docs/pi/llama-cpp.md +118 -0
  29. package/ai_docs/pi/mcp.md +274 -0
  30. package/ai_docs/pi/message-types.md +265 -0
  31. package/ai_docs/pi/models.md +209 -0
  32. package/ai_docs/pi/overview.md +166 -0
  33. package/ai_docs/pi/packages.md +133 -0
  34. package/ai_docs/pi/pi-agent-core.md +568 -0
  35. package/ai_docs/pi/pi-ai.md +1942 -0
  36. package/ai_docs/pi/prompt-templates.md +63 -0
  37. package/ai_docs/pi/providers.md +204 -0
  38. package/ai_docs/pi/quickstart.md +140 -0
  39. package/ai_docs/pi/readme.md +99 -0
  40. package/ai_docs/pi/rpc-commands.md +864 -0
  41. package/ai_docs/pi/rpc-extension-ui.md +204 -0
  42. package/ai_docs/pi/rpc.md +197 -0
  43. package/ai_docs/pi/sdk.md +151 -0
  44. package/ai_docs/pi/security.md +103 -0
  45. package/ai_docs/pi/session-format.md +297 -0
  46. package/ai_docs/pi/sessions.md +70 -0
  47. package/ai_docs/pi/settings.md +173 -0
  48. package/ai_docs/pi/shell-aliases.md +97 -0
  49. package/ai_docs/pi/skills.md +97 -0
  50. package/ai_docs/pi/slash-commands.md +64 -0
  51. package/ai_docs/pi/sources.md +57 -0
  52. package/ai_docs/pi/terminal-setup.md +223 -0
  53. package/ai_docs/pi/termux.md +121 -0
  54. package/ai_docs/pi/themes.md +140 -0
  55. package/ai_docs/pi/tmux.md +59 -0
  56. package/ai_docs/pi/toolkit.md +168 -0
  57. package/ai_docs/pi/tui.md +129 -0
  58. package/ai_docs/pi/usage.md +98 -0
  59. package/ai_docs/pi/virtual-models.md +118 -0
  60. package/ai_docs/pi/windows.md +71 -0
  61. package/apps/dpi-backend/.nvmrc +1 -0
  62. package/apps/dpi-backend/API.md +99 -0
  63. package/apps/dpi-backend/README.md +119 -0
  64. package/apps/dpi-backend/deploy/backend.env.example +9 -0
  65. package/apps/dpi-backend/deploy/dpi-backend.plist +26 -0
  66. package/apps/dpi-backend/deploy/dpi-backend.service +22 -0
  67. package/apps/dpi-backend/package.json +33 -0
  68. package/apps/dpi-backend/prompts/one_sentence.md +3 -0
  69. package/apps/dpi-backend/prompts/smart_ass_opus_fix.md +169 -0
  70. package/apps/dpi-backend/scripts/check-contract.mjs +151 -0
  71. package/apps/dpi-backend/scripts/migrate-playground.ts +66 -0
  72. package/apps/dpi-backend/scripts/record-nested-fixture.mts +154 -0
  73. package/apps/dpi-backend/scripts/token.ts +57 -0
  74. package/apps/dpi-backend/src/addons.ts +139 -0
  75. package/apps/dpi-backend/src/auth.ts +20 -0
  76. package/apps/dpi-backend/src/config.ts +54 -0
  77. package/apps/dpi-backend/src/contract-check.ts +118 -0
  78. package/apps/dpi-backend/src/errors.ts +50 -0
  79. package/apps/dpi-backend/src/extensions.ts +510 -0
  80. package/apps/dpi-backend/src/faux.ts +80 -0
  81. package/apps/dpi-backend/src/history.ts +165 -0
  82. package/apps/dpi-backend/src/idempotency.ts +20 -0
  83. package/apps/dpi-backend/src/jev.ts +43 -0
  84. package/apps/dpi-backend/src/jobs.ts +790 -0
  85. package/apps/dpi-backend/src/main.ts +76 -0
  86. package/apps/dpi-backend/src/model-catalog.ts +180 -0
  87. package/apps/dpi-backend/src/pairing-files.ts +142 -0
  88. package/apps/dpi-backend/src/pairing.ts +459 -0
  89. package/apps/dpi-backend/src/routes.ts +217 -0
  90. package/apps/dpi-backend/src/shared-state.ts +85 -0
  91. package/apps/dpi-backend/src/store.ts +1240 -0
  92. package/apps/dpi-backend/src/streams.ts +256 -0
  93. package/apps/dpi-backend/src/tunnel.ts +955 -0
  94. package/apps/dpi-backend/tsconfig.json +13 -0
  95. package/apps/dpi-playground/README.md +83 -0
  96. package/apps/dpi-playground/apps/dpi-playground-fe/dist/assets/index-CFGc7lHT.js +77 -0
  97. package/apps/dpi-playground/apps/dpi-playground-fe/dist/assets/index-DFdKpwm_.css +1 -0
  98. package/apps/dpi-playground/apps/dpi-playground-fe/dist/icons/anthropic.png +0 -0
  99. package/apps/dpi-playground/apps/dpi-playground-fe/dist/icons/deepseek.png +0 -0
  100. package/apps/dpi-playground/apps/dpi-playground-fe/dist/icons/durable-pi.svg +14 -0
  101. package/apps/dpi-playground/apps/dpi-playground-fe/dist/icons/gemini.png +0 -0
  102. package/apps/dpi-playground/apps/dpi-playground-fe/dist/icons/grok.png +0 -0
  103. package/apps/dpi-playground/apps/dpi-playground-fe/dist/icons/kimi.png +0 -0
  104. package/apps/dpi-playground/apps/dpi-playground-fe/dist/icons/lab.png +0 -0
  105. package/apps/dpi-playground/apps/dpi-playground-fe/dist/icons/meta.png +0 -0
  106. package/apps/dpi-playground/apps/dpi-playground-fe/dist/icons/minimax.png +0 -0
  107. package/apps/dpi-playground/apps/dpi-playground-fe/dist/icons/openai.png +0 -0
  108. package/apps/dpi-playground/apps/dpi-playground-fe/dist/icons/qwen.png +0 -0
  109. package/apps/dpi-playground/apps/dpi-playground-fe/dist/icons/xiaomi.png +0 -0
  110. package/apps/dpi-playground/apps/dpi-playground-fe/dist/icons/zai.png +0 -0
  111. package/apps/dpi-playground/apps/dpi-playground-fe/dist/index.html +16 -0
  112. package/apps/dpi-playground/apps/dpi-playground-fe/index.html +15 -0
  113. package/apps/dpi-playground/apps/dpi-playground-fe/package.json +5 -0
  114. package/apps/dpi-playground/apps/dpi-playground-fe/public/icons/anthropic.png +0 -0
  115. package/apps/dpi-playground/apps/dpi-playground-fe/public/icons/deepseek.png +0 -0
  116. package/apps/dpi-playground/apps/dpi-playground-fe/public/icons/durable-pi.svg +14 -0
  117. package/apps/dpi-playground/apps/dpi-playground-fe/public/icons/gemini.png +0 -0
  118. package/apps/dpi-playground/apps/dpi-playground-fe/public/icons/grok.png +0 -0
  119. package/apps/dpi-playground/apps/dpi-playground-fe/public/icons/kimi.png +0 -0
  120. package/apps/dpi-playground/apps/dpi-playground-fe/public/icons/lab.png +0 -0
  121. package/apps/dpi-playground/apps/dpi-playground-fe/public/icons/meta.png +0 -0
  122. package/apps/dpi-playground/apps/dpi-playground-fe/public/icons/minimax.png +0 -0
  123. package/apps/dpi-playground/apps/dpi-playground-fe/public/icons/openai.png +0 -0
  124. package/apps/dpi-playground/apps/dpi-playground-fe/public/icons/qwen.png +0 -0
  125. package/apps/dpi-playground/apps/dpi-playground-fe/public/icons/xiaomi.png +0 -0
  126. package/apps/dpi-playground/apps/dpi-playground-fe/public/icons/zai.png +0 -0
  127. package/apps/dpi-playground/apps/dpi-playground-fe/src/App.vue +180 -0
  128. package/apps/dpi-playground/apps/dpi-playground-fe/src/client/commands.ts +119 -0
  129. package/apps/dpi-playground/apps/dpi-playground-fe/src/client/composables.ts +129 -0
  130. package/apps/dpi-playground/apps/dpi-playground-fe/src/client/hub.ts +40 -0
  131. package/apps/dpi-playground/apps/dpi-playground-fe/src/client/icon-path.ts +7 -0
  132. package/apps/dpi-playground/apps/dpi-playground-fe/src/client/ids.ts +2 -0
  133. package/apps/dpi-playground/apps/dpi-playground-fe/src/client/jobs.ts +80 -0
  134. package/apps/dpi-playground/apps/dpi-playground-fe/src/client/link-policy.ts +16 -0
  135. package/apps/dpi-playground/apps/dpi-playground-fe/src/client/markdown-policy.ts +115 -0
  136. package/apps/dpi-playground/apps/dpi-playground-fe/src/client/markdown.ts +60 -0
  137. package/apps/dpi-playground/apps/dpi-playground-fe/src/client/pair-close.ts +23 -0
  138. package/apps/dpi-playground/apps/dpi-playground-fe/src/client/pair-handoff.ts +56 -0
  139. package/apps/dpi-playground/apps/dpi-playground-fe/src/client/pair.ts +166 -0
  140. package/apps/dpi-playground/apps/dpi-playground-fe/src/client/qrcode.ts +323 -0
  141. package/apps/dpi-playground/apps/dpi-playground-fe/src/client/signature.ts +146 -0
  142. package/apps/dpi-playground/apps/dpi-playground-fe/src/client/store.ts +373 -0
  143. package/apps/dpi-playground/apps/dpi-playground-fe/src/client/stream.ts +183 -0
  144. package/apps/dpi-playground/apps/dpi-playground-fe/src/components/AgentView.vue +342 -0
  145. package/apps/dpi-playground/apps/dpi-playground-fe/src/components/Composer.vue +198 -0
  146. package/apps/dpi-playground/apps/dpi-playground-fe/src/components/ContextBar.vue +254 -0
  147. package/apps/dpi-playground/apps/dpi-playground-fe/src/components/DeviceCredentialsDialog.vue +40 -0
  148. package/apps/dpi-playground/apps/dpi-playground-fe/src/components/DeviceIcon.vue +56 -0
  149. package/apps/dpi-playground/apps/dpi-playground-fe/src/components/DirBrowser.vue +95 -0
  150. package/apps/dpi-playground/apps/dpi-playground-fe/src/components/ExtensionsPanel.vue +96 -0
  151. package/apps/dpi-playground/apps/dpi-playground-fe/src/components/HelpDialog.vue +69 -0
  152. package/apps/dpi-playground/apps/dpi-playground-fe/src/components/Inspector.vue +425 -0
  153. package/apps/dpi-playground/apps/dpi-playground-fe/src/components/JobCard.vue +221 -0
  154. package/apps/dpi-playground/apps/dpi-playground-fe/src/components/MobileNav.vue +39 -0
  155. package/apps/dpi-playground/apps/dpi-playground-fe/src/components/ModelPicker.vue +71 -0
  156. package/apps/dpi-playground/apps/dpi-playground-fe/src/components/NetworkSidebar.vue +236 -0
  157. package/apps/dpi-playground/apps/dpi-playground-fe/src/components/NewSessionDialog.vue +114 -0
  158. package/apps/dpi-playground/apps/dpi-playground-fe/src/components/OfflinePanel.vue +128 -0
  159. package/apps/dpi-playground/apps/dpi-playground-fe/src/components/PairDeviceWizard.vue +433 -0
  160. package/apps/dpi-playground/apps/dpi-playground-fe/src/components/PromptDialog.vue +89 -0
  161. package/apps/dpi-playground/apps/dpi-playground-fe/src/components/StateEditor.vue +179 -0
  162. package/apps/dpi-playground/apps/dpi-playground-fe/src/components/TasksPanel.vue +280 -0
  163. package/apps/dpi-playground/apps/dpi-playground-fe/src/components/Toast.vue +11 -0
  164. package/apps/dpi-playground/apps/dpi-playground-fe/src/components/TokenManager.vue +175 -0
  165. package/apps/dpi-playground/apps/dpi-playground-fe/src/components/TopBar.vue +114 -0
  166. package/apps/dpi-playground/apps/dpi-playground-fe/src/components/TranscriptItem.vue +152 -0
  167. package/apps/dpi-playground/apps/dpi-playground-fe/src/components/TreeRow.vue +195 -0
  168. package/apps/dpi-playground/apps/dpi-playground-fe/src/components/TunnelStatusBar.vue +107 -0
  169. package/apps/dpi-playground/apps/dpi-playground-fe/src/extensions.css +105 -0
  170. package/apps/dpi-playground/apps/dpi-playground-fe/src/jobs.css +655 -0
  171. package/apps/dpi-playground/apps/dpi-playground-fe/src/main.ts +22 -0
  172. package/apps/dpi-playground/apps/dpi-playground-fe/src/network.css +675 -0
  173. package/apps/dpi-playground/apps/dpi-playground-fe/src/style.css +3203 -0
  174. package/apps/dpi-playground/apps/dpi-playground-fe/tsconfig.json +16 -0
  175. package/apps/dpi-playground/apps/dpi-playground-fe/vite.config.ts +14 -0
  176. package/apps/dpi-playground/apps/dpi-playground-hub/package.json +5 -0
  177. package/apps/dpi-playground/apps/dpi-playground-hub/src/audit.ts +406 -0
  178. package/apps/dpi-playground/apps/dpi-playground-hub/src/client-ip.ts +72 -0
  179. package/apps/dpi-playground/apps/dpi-playground-hub/src/credentials.ts +265 -0
  180. package/apps/dpi-playground/apps/dpi-playground-hub/src/guard.ts +43 -0
  181. package/apps/dpi-playground/apps/dpi-playground-hub/src/http-errors.ts +156 -0
  182. package/apps/dpi-playground/apps/dpi-playground-hub/src/identity.ts +60 -0
  183. package/apps/dpi-playground/apps/dpi-playground-hub/src/limiter.ts +165 -0
  184. package/apps/dpi-playground/apps/dpi-playground-hub/src/main.ts +460 -0
  185. package/apps/dpi-playground/apps/dpi-playground-hub/src/monitor.ts +320 -0
  186. package/apps/dpi-playground/apps/dpi-playground-hub/src/mux.ts +277 -0
  187. package/apps/dpi-playground/apps/dpi-playground-hub/src/pairing.ts +816 -0
  188. package/apps/dpi-playground/apps/dpi-playground-hub/src/proxy.ts +128 -0
  189. package/apps/dpi-playground/apps/dpi-playground-hub/src/rotation.ts +583 -0
  190. package/apps/dpi-playground/apps/dpi-playground-hub/src/seed.ts +11 -0
  191. package/apps/dpi-playground/apps/dpi-playground-hub/src/transport.ts +108 -0
  192. package/apps/dpi-playground/apps/dpi-playground-hub/src/tunnel-token.ts +11 -0
  193. package/apps/dpi-playground/apps/dpi-playground-hub/src/tunnel-transport.ts +560 -0
  194. package/apps/dpi-playground/apps/dpi-playground-hub/src/tunnel.ts +873 -0
  195. package/apps/dpi-playground/apps/dpi-playground-hub/tsconfig.json +13 -0
  196. package/apps/dpi-playground/package.json +37 -0
  197. package/apps/dpi-sdk/README.md +74 -0
  198. package/apps/dpi-sdk/package.json +20 -0
  199. package/apps/dpi-sdk/scripts/net.ts +73 -0
  200. package/apps/dpi-sdk/src/api.ts +631 -0
  201. package/apps/dpi-sdk/src/app-state.ts +111 -0
  202. package/apps/dpi-sdk/src/client.ts +330 -0
  203. package/apps/dpi-sdk/src/errors.ts +16 -0
  204. package/apps/dpi-sdk/src/jobs.ts +99 -0
  205. package/apps/dpi-sdk/src/models.ts +472 -0
  206. package/apps/dpi-sdk/src/node/chunk.ts +488 -0
  207. package/apps/dpi-sdk/src/node/config.ts +175 -0
  208. package/apps/dpi-sdk/src/node/drivers.ts +402 -0
  209. package/apps/dpi-sdk/src/node/ops.ts +114 -0
  210. package/apps/dpi-sdk/src/node/private-dir.ts +48 -0
  211. package/apps/dpi-sdk/src/node/registry-migration.ts +549 -0
  212. package/apps/dpi-sdk/src/node/registry.ts +390 -0
  213. package/apps/dpi-sdk/src/node/sse-writer.ts +151 -0
  214. package/apps/dpi-sdk/src/protocol/backoff.ts +31 -0
  215. package/apps/dpi-sdk/src/protocol/limits.ts +199 -0
  216. package/apps/dpi-sdk/src/protocol/pairing.ts +311 -0
  217. package/apps/dpi-sdk/src/protocol/rest-routes.ts +105 -0
  218. package/apps/dpi-sdk/src/protocol/tunnel.ts +331 -0
  219. package/apps/dpi-sdk/src/sse.ts +117 -0
  220. package/apps/dpi-sdk/src/transcript.ts +291 -0
  221. package/apps/dpi-sdk/tsconfig.json +13 -0
  222. package/apps/dpi-sdk/w +20 -0
  223. package/apps/dpi-tui/README.md +54 -0
  224. package/apps/dpi-tui/bin/dpi-tui.mjs +5 -0
  225. package/apps/dpi-tui/package.json +20 -0
  226. package/apps/dpi-tui/src/app.ts +433 -0
  227. package/apps/dpi-tui/src/commands.ts +151 -0
  228. package/apps/dpi-tui/src/main.ts +122 -0
  229. package/apps/dpi-tui/src/pairing.ts +287 -0
  230. package/apps/dpi-tui/src/pickers.ts +238 -0
  231. package/apps/dpi-tui/src/render.ts +164 -0
  232. package/apps/dpi-tui/src/theme.ts +57 -0
  233. package/apps/dpi-tui/tsconfig.json +14 -0
  234. package/bin/device.mjs +5 -0
  235. package/bin/hub.mjs +5 -0
  236. package/bin/tui.mjs +5 -0
  237. package/deploy/Caddyfile +120 -0
  238. package/docs/verification.md +102 -0
  239. package/dpi-network.example.yaml +62 -0
  240. package/images/01_durable_agent_network.svg +185 -0
  241. package/images/02_pi_made_durable.svg +204 -0
  242. package/images/03_restart_mid_tool.svg +168 -0
  243. package/images/07_one_api_every_client.svg +123 -0
  244. package/images/08_device_session_agent.svg +123 -0
  245. package/images/09_compact_keep_history.svg +143 -0
  246. package/images/10_shared_app_state.svg +177 -0
  247. package/images/11_durable_jobs_wait.svg +141 -0
  248. package/images/12_tools_change_mid_run.svg +231 -0
  249. package/images/13_hook_blocks_tool.svg +122 -0
  250. package/images/14_fork_at_any_entry.svg +119 -0
  251. package/images/15_web_client.png +0 -0
  252. package/images/16_terminal_client.svg +43 -0
  253. package/images/17_devices_are_data.svg +182 -0
  254. package/images/18_should_every_agent_be_durable.svg +118 -0
  255. package/images/19_sqlite_on_every_device.svg +155 -0
  256. package/images/20_admission_and_effects.svg +134 -0
  257. package/justfile +33 -0
  258. package/package.json +41 -4
  259. package/scripts/verify.mjs +210 -0
@@ -0,0 +1,280 @@
1
+ <script setup lang="ts">
2
+ import { computed, ref } from "vue";
3
+ import {
4
+ Activity,
5
+ Bot,
6
+ Clock3,
7
+ Layers,
8
+ Octagon,
9
+ Play,
10
+ Repeat,
11
+ ShieldCheck,
12
+ Workflow,
13
+ Wrench,
14
+ } from "lucide-vue-next";
15
+ import type { TaskNode, TaskOutcome } from "@sdk/api";
16
+ import { jobRunDone } from "@sdk/jobs";
17
+ import { message } from "../client/hub";
18
+ import { agentHandle, sessionHandle, tasks } from "../client/store";
19
+ import {
20
+ decide,
21
+ jobDef,
22
+ jobDefs,
23
+ jobRuns,
24
+ openAgent,
25
+ pending,
26
+ revealRun,
27
+ startJob,
28
+ } from "../client/jobs";
29
+
30
+ const error = ref("");
31
+ const stopping = ref(false);
32
+ const recent = ref<TaskOutcome[]>([]);
33
+ const inspection = ref<unknown>();
34
+
35
+ const status = (value: string) =>
36
+ ({
37
+ running: "Running",
38
+ waiting: "Waiting",
39
+ completed: "Completed",
40
+ aborted: "Stopped",
41
+ declined: "Declined",
42
+ failed: "Failed",
43
+ pending: "Queued",
44
+ completing: "Finishing",
45
+ terminal: "Finished",
46
+ })[value] ?? value;
47
+
48
+ /** The newest run of each job, for its row in the list. */
49
+ const latest = computed(
50
+ () => new Map(jobDefs.value.map((d) => [d.id, jobRuns.value.find((r) => r.job === d.id)])),
51
+ );
52
+
53
+ // ------------------------------------------------------------------ the live task tree
54
+
55
+ type Row = { task: TaskNode & { effectiveBackground: boolean }; depth: number };
56
+ const rows = computed<Row[]>(() => {
57
+ const all = tasks.value;
58
+ const ids = new Set(all.map((t) => t.id));
59
+ const children = new Map<number | undefined, typeof all>();
60
+ for (const t of all) {
61
+ const parent = t.owner !== undefined && ids.has(t.owner) ? t.owner : undefined;
62
+ children.set(parent, [...(children.get(parent) ?? []), t]);
63
+ }
64
+ const out: Row[] = [];
65
+ const walk = (parent: number | undefined, depth: number) => {
66
+ for (const task of (children.get(parent) ?? []).sort((a, b) => a.id - b.id)) {
67
+ out.push({ task, depth });
68
+ walk(task.id, depth + 1);
69
+ }
70
+ };
71
+ walk(undefined, 0);
72
+ return out;
73
+ });
74
+ const kindOf = (t: TaskNode) =>
75
+ t.kind === "pi.tool"
76
+ ? { icon: Wrench, name: t.label ?? "Tool call", what: "tool call" }
77
+ : t.kind === "pi.generation"
78
+ ? { icon: Bot, name: "Model response", what: "generation" }
79
+ : t.kind === "pi.compaction"
80
+ ? { icon: Layers, name: "Compaction", what: "compaction" }
81
+ : t.kind.startsWith("demo.job.")
82
+ ? {
83
+ icon: Workflow,
84
+ name: t.label ?? jobDef(t.kind.slice(9))?.title ?? t.kind,
85
+ what: "durable job",
86
+ }
87
+ : t.kind === "demo.job-approval"
88
+ ? { icon: ShieldCheck, name: "Approval", what: "waits for you" }
89
+ : { icon: Activity, name: t.label ?? t.kind, what: t.kind };
90
+ const phaseOf = (t: TaskNode) => {
91
+ if (!t.state.phase) return "";
92
+ if (t.kind.startsWith("demo.job."))
93
+ return (
94
+ jobDef(t.kind.slice(9))?.phases.find((p) => p.id === t.state.phase)?.label ?? t.state.phase
95
+ );
96
+ return t.state.phase;
97
+ };
98
+
99
+ async function stopAll() {
100
+ if (stopping.value || !agentHandle.value) return;
101
+ stopping.value = true;
102
+ error.value = "";
103
+ try {
104
+ await agentHandle.value.abort(true);
105
+ } catch (e) {
106
+ error.value = message(e);
107
+ } finally {
108
+ stopping.value = false;
109
+ }
110
+ }
111
+
112
+ // Recent outcomes and recovery inspection are read on demand, not streamed.
113
+ async function loadRuntime(e: Event) {
114
+ const handle = sessionHandle.value;
115
+ if (!(e.target as HTMLDetailsElement).open || !handle) return;
116
+ try {
117
+ const [r, i] = await Promise.all([handle.tasks("terminal", 40), handle.inspect()]);
118
+ recent.value = r;
119
+ inspection.value = i;
120
+ } catch (err) {
121
+ error.value = message(err);
122
+ }
123
+ }
124
+ const outcomes = computed(() =>
125
+ recent.value
126
+ .filter((task) => task.state.status === "terminal")
127
+ .slice(-12)
128
+ .reverse(),
129
+ );
130
+ </script>
131
+
132
+ <template>
133
+ <div class="tasks-panel">
134
+ <div class="panel-heading">
135
+ <h2>Tasks</h2>
136
+ <span class="tag">{{ tasks.length }} active</span>
137
+ </div>
138
+ <p v-if="error" class="message-error" role="alert">{{ error }}</p>
139
+
140
+ <section class="task-section" aria-label="Durable jobs">
141
+ <h3><Repeat :size="18" />Durable jobs</h3>
142
+ <p class="hint">
143
+ Repeatable workflows with named phases. Every run follows the same phases and saves a
144
+ checkpoint after each one. Start one here, or ask your agent: it finds these with
145
+ <code>list_durable_jobs</code>, starts them with <code>run_durable_job</code> and checks
146
+ them with <code>check_durable_job</code>. Runs play out live in the chat.
147
+ </p>
148
+ <p v-if="!jobDefs.length" class="task-empty">
149
+ This device has no durable jobs. Deploy the latest backend to it.
150
+ </p>
151
+ <article v-for="def in jobDefs" :key="def.id" class="job-def" :data-job="def.id">
152
+ <div class="job-def-head">
153
+ <Workflow :size="18" />
154
+ <strong>{{ def.title }}</strong>
155
+ <span v-if="def.approval" class="tag">needs approval</span>
156
+ <span class="tag">~{{ def.seconds }}s</span>
157
+ </div>
158
+ <p>{{ def.summary }}</p>
159
+ <ol class="job-chain" :aria-label="`${def.title} phases`">
160
+ <li v-for="(phase, i) in def.phases" :key="phase.id" :title="phase.summary">
161
+ <span>{{ i + 1 }}</span
162
+ >{{ phase.label }}<Repeat v-if="phase.kind === 'repeat'" :size="13" /><ShieldCheck
163
+ v-if="phase.kind === 'approval'"
164
+ :size="13"
165
+ />
166
+ </li>
167
+ </ol>
168
+ <div class="job-def-meta">
169
+ <code v-for="input in def.inputs" :key="input.name" :title="input.description"
170
+ >{{ input.name }}={{ input.default }}</code
171
+ >
172
+ <code :title="`Each run saves its result to App State ${def.resultPath}`"
173
+ >→ {{ def.resultPath }}</code
174
+ >
175
+ </div>
176
+ <div v-if="latest.get(def.id)" class="job-def-last">
177
+ <span
178
+ >Run #{{ latest.get(def.id)!.run }}:
179
+ <strong>{{ status(latest.get(def.id)!.status) }}</strong
180
+ ><template v-if="!jobRunDone(latest.get(def.id)!)">
181
+ in
182
+ {{
183
+ latest.get(def.id)!.phases.find((p) => p.id === latest.get(def.id)!.phase)?.label
184
+ }}</template
185
+ ></span
186
+ >
187
+ <button class="text-button" @click="revealRun(latest.get(def.id)!.id)">
188
+ Show in chat
189
+ </button>
190
+ <template v-if="latest.get(def.id)!.approval === 'pending'">
191
+ <button
192
+ class="primary small"
193
+ :disabled="!!pending"
194
+ @click="decide(latest.get(def.id)!, true)"
195
+ >
196
+ Approve
197
+ </button>
198
+ <button
199
+ class="secondary"
200
+ :disabled="!!pending"
201
+ @click="decide(latest.get(def.id)!, false)"
202
+ >
203
+ Decline
204
+ </button>
205
+ </template>
206
+ </div>
207
+ <button
208
+ class="secondary job-def-run"
209
+ :disabled="!!pending || !agentHandle"
210
+ @click="startJob(def)"
211
+ >
212
+ <Play :size="16" />Run {{ def.title }}
213
+ </button>
214
+ </article>
215
+ </section>
216
+
217
+ <section class="task-section" aria-label="Agent activity">
218
+ <div class="panel-heading">
219
+ <h3>Agent activity</h3>
220
+ <Activity :size="18" />
221
+ </div>
222
+ <p class="hint">
223
+ Every model response and every tool call is its own durable task with an id, an owner and a
224
+ checkpoint. Follow them live here; after a restart each one resumes or reports how it ended.
225
+ </p>
226
+ <p v-if="!rows.length" class="task-empty">Nothing running. Send a message or run a job.</p>
227
+ <ul v-else class="task-tree">
228
+ <li
229
+ v-for="{ task, depth } in rows"
230
+ :key="task.id"
231
+ class="task-row"
232
+ :style="{ '--depth': depth }"
233
+ :data-task-kind="task.kind"
234
+ >
235
+ <component :is="kindOf(task).icon" :size="16" />
236
+ <strong>{{ kindOf(task).name }}</strong>
237
+ <span class="tag">{{ status(task.state.status) }}</span>
238
+ <span v-if="phaseOf(task)" class="task-phase">{{ phaseOf(task) }}</span>
239
+ <small
240
+ >#{{ task.id }} {{ kindOf(task).what }}, agent #{{ task.conversationId
241
+ }}{{ task.effectiveBackground ? ", background" : "" }}</small
242
+ >
243
+ <button
244
+ v-for="cid in task.conversations"
245
+ :key="cid"
246
+ class="text-button"
247
+ @click="openAgent(cid)"
248
+ >
249
+ Open agent #{{ cid }}
250
+ </button>
251
+ </li>
252
+ </ul>
253
+ <p class="hint">
254
+ Interrupt or Esc stops foreground work and what it owns. Stop all also stops background
255
+ runs.
256
+ </p>
257
+ <button class="secondary danger" :disabled="!tasks.length || stopping" @click="stopAll">
258
+ <Octagon :size="18" />Stop all
259
+ </button>
260
+ </section>
261
+
262
+ <details class="raw task-section" @toggle="loadRuntime">
263
+ <summary><Clock3 :size="18" />Runtime details</summary>
264
+ <p class="hint">
265
+ Recent task results and recovery records, read from the device when opened.
266
+ </p>
267
+ <details v-for="task in outcomes" :key="task.id" class="raw">
268
+ <summary>
269
+ {{ task.kind }} #{{ task.id
270
+ }}<span>{{ status(task.state.outcome?.status ?? task.state.status) }}</span>
271
+ </summary>
272
+ <pre>{{ JSON.stringify(task.state.outcome, null, 2) }}</pre>
273
+ </details>
274
+ <details class="raw">
275
+ <summary>Recovery inspection</summary>
276
+ <pre>{{ JSON.stringify(inspection, null, 2) }}</pre>
277
+ </details>
278
+ </details>
279
+ </div>
280
+ </template>
@@ -0,0 +1,11 @@
1
+ <script setup lang="ts">
2
+ // Toast: bottom-center pill, bound to the existing ui.toast / notify() timer in the store.
3
+ import { Check } from "lucide-vue-next";
4
+ import { ui } from "../client/store";
5
+ </script>
6
+
7
+ <template>
8
+ <Transition name="toast">
9
+ <div v-if="ui.toast" class="toast" role="status"><Check :size="18" />{{ ui.toast }}</div>
10
+ </Transition>
11
+ </template>
@@ -0,0 +1,175 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * P2-25 — TokenManager: the public view of a device pairing. Only public
4
+ * metadata ever renders here: pairing status, key fingerprint, paired-at,
5
+ * credential generation, rotationPending. No token, no hash, no secret.
6
+ *
7
+ * Honesty contract:
8
+ * - rotate 202 = queued intent → badge "rotation queued (waiting device ACK)";
9
+ * nothing says "delivered" until the device's new generation appears.
10
+ * - revoke asks for confirmation; a 202 says the hub committed — it does NOT
11
+ * claim the device files were wiped (that is only known from the device's
12
+ * own matching notice / auth failure on a later connection).
13
+ */
14
+ import { computed, ref } from "vue";
15
+ import { KeyRound, RefreshCw, ShieldOff } from "lucide-vue-next";
16
+ import type { NetworkDevice } from "@sdk/api";
17
+ import { pairingAdmin } from "../client/pair";
18
+ import { message } from "../client/hub";
19
+ import { ago, notify, ui } from "../client/store";
20
+
21
+ const props = defineProps<{ device: NetworkDevice }>();
22
+ const busy = ref(false);
23
+ const error = ref("");
24
+
25
+ const pairing = computed(() => props.device.pairing);
26
+
27
+ async function rotate(): Promise<void> {
28
+ if (busy.value) return;
29
+ busy.value = true;
30
+ error.value = "";
31
+ try {
32
+ await pairingAdmin.rotate(props.device.id);
33
+ notify("Rotation queued — it completes when the device acknowledges the new credential.");
34
+ } catch (e) {
35
+ error.value = message(e);
36
+ } finally {
37
+ busy.value = false;
38
+ }
39
+ }
40
+
41
+ async function revoke(): Promise<void> {
42
+ if (busy.value) return;
43
+ ui.dialog = {
44
+ kind: "prompt",
45
+ title: `Revoke ${props.device.id}?`,
46
+ hint: "Every tunnel credential for this device dies at commit and the live tunnel is cut within a second. The device cleans its own files the next time it talks to the hub.",
47
+ submit: "Revoke",
48
+ danger: true,
49
+ run: async () => {
50
+ await pairingAdmin.revoke(props.device.id, "user");
51
+ notify(
52
+ "Revoke committed on the hub. The tunnel is cut; the device cleans up on its next contact.",
53
+ );
54
+ },
55
+ };
56
+ }
57
+ </script>
58
+
59
+ <template>
60
+ <section v-if="pairing" class="token-manager" aria-label="Credential manager">
61
+ <header>
62
+ <KeyRound :size="15" />
63
+ <h3>Credential</h3>
64
+ <span class="pairing-status" :class="pairing.status">{{ pairing.status }}</span>
65
+ </header>
66
+ <dl>
67
+ <dt>Key fingerprint</dt>
68
+ <dd>
69
+ <code class="credential-fingerprint">{{ pairing.keyFingerprint }}</code>
70
+ </dd>
71
+ <dt>Paired</dt>
72
+ <dd :title="new Date(pairing.pairedAt).toISOString()">{{ ago(pairing.pairedAt) }}</dd>
73
+ <dt>Generation</dt>
74
+ <dd>{{ pairing.credentialGeneration }}</dd>
75
+ </dl>
76
+ <p v-if="pairing.rotationPending" class="rotation-badge" role="status">
77
+ <RefreshCw :size="13" class="spin" /> rotation queued — waiting for the device to ACK
78
+ </p>
79
+ <div class="token-actions">
80
+ <button
81
+ v-if="pairing.status === 'paired'"
82
+ class="small"
83
+ :disabled="busy || pairing.rotationPending"
84
+ @click="rotate"
85
+ >
86
+ <RefreshCw :size="14" /> Rotate credential
87
+ </button>
88
+ <button
89
+ v-if="pairing.status === 'paired'"
90
+ class="small danger"
91
+ :disabled="busy"
92
+ @click="revoke"
93
+ >
94
+ <ShieldOff :size="14" /> Revoke
95
+ </button>
96
+ </div>
97
+ <p v-if="error" class="message-error" role="alert">{{ error }}</p>
98
+ </section>
99
+ </template>
100
+
101
+ <style>
102
+ .token-manager {
103
+ border: 1px solid var(--border, #333);
104
+ border-radius: 8px;
105
+ padding: 0.6rem 0.8rem;
106
+ display: flex;
107
+ flex-direction: column;
108
+ gap: 0.5rem;
109
+ font-size: 0.8rem;
110
+ }
111
+ .token-manager header {
112
+ display: flex;
113
+ align-items: center;
114
+ gap: 0.4rem;
115
+ }
116
+ .token-manager h3 {
117
+ margin: 0;
118
+ font-size: 0.8rem;
119
+ text-transform: uppercase;
120
+ letter-spacing: 0.08em;
121
+ }
122
+ .token-manager .pairing-status {
123
+ margin-left: auto;
124
+ font-size: 0.7rem;
125
+ padding: 0.05rem 0.5rem;
126
+ border-radius: 999px;
127
+ border: 1px solid var(--border, #333);
128
+ }
129
+ .token-manager .pairing-status.paired {
130
+ color: var(--ok, #6cc070);
131
+ }
132
+ .token-manager .pairing-status.revoked {
133
+ color: var(--warn, #e0b040);
134
+ }
135
+ .token-manager dl {
136
+ display: grid;
137
+ grid-template-columns: auto 1fr;
138
+ gap: 0.15rem 0.8rem;
139
+ margin: 0;
140
+ }
141
+ .token-manager dt {
142
+ color: var(--muted, #888);
143
+ }
144
+ .token-manager dd {
145
+ margin: 0;
146
+ min-width: 0;
147
+ overflow: hidden;
148
+ text-overflow: ellipsis;
149
+ }
150
+ .token-manager .credential-fingerprint {
151
+ white-space: normal;
152
+ overflow-wrap: anywhere;
153
+ }
154
+ .token-manager .rotation-badge {
155
+ display: flex;
156
+ align-items: center;
157
+ gap: 0.35rem;
158
+ margin: 0;
159
+ color: var(--warn, #e0b040);
160
+ font-size: 0.75rem;
161
+ }
162
+ .token-manager .token-actions {
163
+ display: flex;
164
+ gap: 0.5rem;
165
+ flex-wrap: wrap;
166
+ }
167
+ .token-manager .spin {
168
+ animation: tm-spin 1.2s linear infinite;
169
+ }
170
+ @keyframes tm-spin {
171
+ to {
172
+ transform: rotate(360deg);
173
+ }
174
+ }
175
+ </style>
@@ -0,0 +1,114 @@
1
+ <script setup lang="ts">
2
+ // TopBar: the top strip, bound to the real client store — device/session/agent counts,
3
+ // stream state, and the panel/new-session actions. Presentation only; all data comes
4
+ // from client/store.ts.
5
+ import { computed, ref } from "vue";
6
+ import { CircleHelp, Network, PanelLeft, PanelRight, Plus } from "lucide-vue-next";
7
+ import {
8
+ attempt,
9
+ network,
10
+ notify,
11
+ online,
12
+ select,
13
+ selected,
14
+ sessionHandle,
15
+ streamState,
16
+ ui,
17
+ showInspector,
18
+ hideInspector,
19
+ } from "../client/store";
20
+
21
+ const onlineCount = computed(
22
+ () => network.devices.value.filter((d) => d.status === "online").length,
23
+ );
24
+ const sessionCount = computed(() =>
25
+ network.devices.value.reduce((n, d) => n + (d.tree?.sessions.length ?? 0), 0),
26
+ );
27
+ const agentCount = computed(() =>
28
+ network.devices.value.reduce(
29
+ (n, d) => n + (d.tree?.sessions.reduce((m, s) => m + s.agents.length, 0) ?? 0),
30
+ 0,
31
+ ),
32
+ );
33
+ const creatingAgent = ref(false);
34
+
35
+ async function newAgent() {
36
+ const handle = sessionHandle.value;
37
+ const t = selected.value;
38
+ if (!handle || !t || creatingAgent.value) return;
39
+ creatingAgent.value = true;
40
+ const created = await attempt(() => handle.createAgent({}));
41
+ creatingAgent.value = false;
42
+ if (created) {
43
+ select({ ...t, agentId: created.id });
44
+ notify("New agent. Same session, same shared App State.");
45
+ }
46
+ }
47
+
48
+ const emit = defineEmits<{ newSession: [] }>();
49
+ </script>
50
+
51
+ <template>
52
+ <header class="topbar">
53
+ <div class="brand">
54
+ <button
55
+ class="icon-button tree-toggle"
56
+ aria-label="Toggle network"
57
+ title="Network"
58
+ @click="ui.drawer = !ui.drawer"
59
+ >
60
+ <PanelLeft :size="19" /></button
61
+ ><img class="brandmark" src="/icons/durable-pi.svg" alt="Durable Pi" /><strong
62
+ >Durable Pi</strong
63
+ >
64
+ </div>
65
+ <span class="topbar-sep" aria-hidden="true"></span>
66
+ <div class="daemon" data-testid="topbar-stats">
67
+ <span class="dot" :class="{ off: !onlineCount }" aria-hidden="true"></span>
68
+ <span
69
+ class="network-summary"
70
+ :title="`${onlineCount} of ${network.devices.value.length} devices online`"
71
+ ><Network :size="16" />{{ onlineCount }} of {{ network.devices.value.length }} online</span
72
+ ><span class="sep" aria-hidden="true">·</span
73
+ ><span
74
+ ><b>{{ sessionCount }}</b> sessions</span
75
+ ><span class="sep" aria-hidden="true">·</span
76
+ ><span
77
+ ><b>{{ agentCount }}</b> agents</span
78
+ >
79
+ </div>
80
+ <div class="top-actions">
81
+ <span class="chip connection" :class="{ accent: streamState === 'live' }"
82
+ ><span class="dot" :class="{ off: streamState !== 'live' }" aria-hidden="true" />{{
83
+ streamState === "live" ? "Connected" : "Reconnecting"
84
+ }}</span
85
+ ><button
86
+ class="icon-button"
87
+ title="How this works"
88
+ aria-label="How this works"
89
+ @click="ui.dialog = { kind: 'help' }"
90
+ >
91
+ <CircleHelp :size="19" /></button
92
+ ><button
93
+ class="secondary small new-agent-button"
94
+ :disabled="!selected || !online || creatingAgent"
95
+ title="Start a fresh agent in this session"
96
+ @click="newAgent"
97
+ >
98
+ <Plus :size="18" />New agent
99
+ </button>
100
+ <button class="primary small" @click="emit('newSession')">
101
+ <Plus :size="18" />New session
102
+ </button>
103
+ <button
104
+ class="icon-button inspector-toggle"
105
+ aria-label="Toggle inspector"
106
+ title="Inspector"
107
+ :disabled="!selected || !online"
108
+ @click="ui.inspectorOverlay ? hideInspector() : showInspector()"
109
+ >
110
+ <PanelRight :size="19" />
111
+ </button>
112
+ </div>
113
+ </header>
114
+ </template>