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,254 @@
1
+ <script setup lang="ts">
2
+ // The selected agent's context window: how full it is, where automatic compaction starts in the background, and
3
+ // where the next request waits for a summary. Both markers drag (or take arrow keys) and save the session's
4
+ // thresholds, which pi-durable reads at its next request.
5
+ import { computed, onUnmounted, ref, watch } from "vue";
6
+ import { Info, X } from "lucide-vue-next";
7
+ import {
8
+ compactionThresholds,
9
+ defaultCompaction,
10
+ type CompactionSettings,
11
+ type WireAssistantMessage,
12
+ } from "@sdk/api";
13
+ import {
14
+ agentState,
15
+ attempt,
16
+ currentModel,
17
+ sessionHandle,
18
+ sessionSummary,
19
+ ui,
20
+ } from "../client/store";
21
+
22
+ const STEP = 512;
23
+ const MIN_AUTO = 4096;
24
+ const GAP = 1024;
25
+
26
+ const k = (n: number) =>
27
+ n >= 1_000_000
28
+ ? `${(n / 1_000_000).toFixed(2)}M`
29
+ : n >= 1000
30
+ ? `${(n / 1000).toFixed(1)}k`
31
+ : String(n);
32
+ const window = computed(() => currentModel.value?.contextWindow ?? 0);
33
+
34
+ /** The last reply's prompt plus output: what the next request starts from. */
35
+ const used = computed(() => {
36
+ const entries = agentState.value.entries;
37
+ for (let i = entries.length - 1; i >= 0; i--) {
38
+ if (entries[i].kind !== "pi.assistant") continue;
39
+ const reply = entries[i].model?.find((m) => m.role === "assistant") as
40
+ | WireAssistantMessage
41
+ | undefined;
42
+ const u = reply?.usage;
43
+ if (u) return u.totalTokens || u.input + u.output + u.cacheRead + u.cacheWrite;
44
+ }
45
+ return 0;
46
+ });
47
+ const compacting = computed(() => agentState.value.compactions.length > 0);
48
+
49
+ // A saved value shows at once; the stream's session summary confirms it a moment later.
50
+ const saved = ref<CompactionSettings>();
51
+ const settings = computed<CompactionSettings>(
52
+ () => saved.value ?? sessionSummary.value?.compaction ?? defaultCompaction(window.value),
53
+ );
54
+ watch(
55
+ () => JSON.stringify(sessionSummary.value?.compaction),
56
+ (now) => {
57
+ if (saved.value && now === JSON.stringify(saved.value)) saved.value = undefined;
58
+ },
59
+ );
60
+
61
+ const draft = ref<{ auto?: number; wait: number }>();
62
+ // The details live in an app dialog, so Escape closes them instead of interrupting the agent.
63
+ const detailsOpen = computed(() => ui.dialog?.kind === "compaction");
64
+ const closeDetails = () => void (ui.dialog = undefined);
65
+ onUnmounted(() => detailsOpen.value && closeDetails());
66
+ const marks = computed(() => draft.value ?? compactionThresholds(window.value, settings.value));
67
+ const pct = (n: number) => `${Math.min(100, Math.max(0, (100 * n) / (window.value || 1)))}%`;
68
+ const fullness = computed(() => Math.round((100 * used.value) / (window.value || 1)));
69
+ /** Labels near either end grow inward so they stay inside the panel. */
70
+ const align = (n: number) => {
71
+ const at = n / (window.value || 1);
72
+ return at < 0.35 ? "start" : at > 0.65 ? "end" : "center";
73
+ };
74
+
75
+ const clampWait = (w: number, auto?: number) =>
76
+ Math.min(window.value - GAP, Math.max((auto ?? MIN_AUTO) + GAP, w));
77
+ const clampAuto = (a: number, wait: number) => Math.min(wait - GAP, Math.max(MIN_AUTO, a));
78
+
79
+ function move(which: "auto" | "wait", tokens: number) {
80
+ const current = marks.value;
81
+ const t = Math.round(tokens / STEP) * STEP;
82
+ draft.value =
83
+ which === "wait"
84
+ ? { auto: current.auto, wait: clampWait(t, current.auto) }
85
+ : { auto: clampAuto(t, current.wait), wait: current.wait };
86
+ }
87
+
88
+ async function save(patch: Partial<CompactionSettings>) {
89
+ const handle = sessionHandle.value;
90
+ if (!handle) return;
91
+ const result = await attempt(() => handle.setCompaction(patch));
92
+ if (result) saved.value = result;
93
+ draft.value = undefined;
94
+ }
95
+
96
+ function commit() {
97
+ const d = draft.value;
98
+ if (!d) return;
99
+ void save({
100
+ reserveTokens: window.value - d.wait,
101
+ ...(d.auto !== undefined ? { backgroundTokens: d.wait - d.auto } : {}),
102
+ });
103
+ }
104
+
105
+ const bar = ref<HTMLElement>();
106
+ let dragging: "auto" | "wait" | undefined;
107
+ function down(which: "auto" | "wait", e: PointerEvent) {
108
+ dragging = which;
109
+ (e.currentTarget as HTMLElement).setPointerCapture(e.pointerId);
110
+ }
111
+ function drag(e: PointerEvent) {
112
+ if (!dragging || !bar.value) return;
113
+ const rect = bar.value.getBoundingClientRect();
114
+ move(dragging, ((e.clientX - rect.left) / rect.width) * window.value);
115
+ }
116
+ function up() {
117
+ if (!dragging) return;
118
+ dragging = undefined;
119
+ commit();
120
+ }
121
+ function key(which: "auto" | "wait", e: KeyboardEvent) {
122
+ const delta =
123
+ (e.shiftKey ? 8192 : 1024) *
124
+ (e.key === "ArrowRight" || e.key === "ArrowUp"
125
+ ? 1
126
+ : e.key === "ArrowLeft" || e.key === "ArrowDown"
127
+ ? -1
128
+ : 0);
129
+ if (!delta) return;
130
+ e.preventDefault();
131
+ const current = marks.value;
132
+ move(which, (which === "auto" ? (current.auto ?? MIN_AUTO) : current.wait) + delta);
133
+ }
134
+ </script>
135
+
136
+ <template>
137
+ <section class="context-section" aria-labelledby="context-title">
138
+ <div class="section-label">
139
+ <span id="context-title">CONTEXT</span>
140
+ <button
141
+ v-if="window"
142
+ class="context-details-toggle"
143
+ @click="ui.dialog = { kind: 'compaction' }"
144
+ >
145
+ <Info :size="16" />Details
146
+ </button>
147
+ </div>
148
+ <div v-if="!window" class="hint">Pick a model to see its context window.</div>
149
+ <template v-else>
150
+ <div class="context-numbers">
151
+ <strong>{{ k(used) }}</strong
152
+ ><span>of {{ k(window) }} tokens</span><span class="context-pct">{{ fullness }}%</span>
153
+ <span v-if="compacting" class="context-compacting"><i />Compacting</span>
154
+ </div>
155
+ <div
156
+ ref="bar"
157
+ class="context-bar"
158
+ :class="{ off: !settings.enabled }"
159
+ @pointermove="drag"
160
+ @pointerup="up"
161
+ @pointercancel="up"
162
+ >
163
+ <div
164
+ class="context-fill"
165
+ :class="{ hot: marks.auto !== undefined && used >= marks.auto }"
166
+ :style="{ width: pct(used) }"
167
+ />
168
+ <div
169
+ class="context-reserve"
170
+ :style="{ left: pct(marks.wait) }"
171
+ title="Reserved for the answer"
172
+ />
173
+ <button
174
+ v-if="marks.auto !== undefined"
175
+ class="context-marker auto"
176
+ :class="`label-${align(marks.auto)}`"
177
+ role="slider"
178
+ aria-label="Automatic compaction starts at"
179
+ :aria-valuemin="MIN_AUTO"
180
+ :aria-valuemax="marks.wait - GAP"
181
+ :aria-valuenow="marks.auto"
182
+ :aria-valuetext="`${k(marks.auto)} tokens`"
183
+ :style="{ left: pct(marks.auto) }"
184
+ @pointerdown="down('auto', $event)"
185
+ @keydown="key('auto', $event)"
186
+ @keyup="commit"
187
+ @blur="commit"
188
+ >
189
+ <span class="context-marker-label">Auto-compact at {{ k(marks.auto) }}</span>
190
+ </button>
191
+ <button
192
+ class="context-marker wait"
193
+ :class="`label-${align(marks.wait)}`"
194
+ role="slider"
195
+ aria-label="The next request waits for a summary at"
196
+ :aria-valuemin="(marks.auto ?? MIN_AUTO) + GAP"
197
+ :aria-valuemax="window - GAP"
198
+ :aria-valuenow="marks.wait"
199
+ :aria-valuetext="`${k(marks.wait)} tokens, ${k(window - marks.wait)} reserved`"
200
+ :style="{ left: pct(marks.wait) }"
201
+ @pointerdown="down('wait', $event)"
202
+ @keydown="key('wait', $event)"
203
+ @keyup="commit"
204
+ @blur="commit"
205
+ >
206
+ <span class="context-marker-label">Reserve at {{ k(marks.wait) }}</span>
207
+ </button>
208
+ </div>
209
+ <p class="hint context-hint">
210
+ <template v-if="settings.enabled"
211
+ >Compacts in the background at <strong>{{ k(marks.auto ?? marks.wait) }}</strong> and
212
+ waits at <strong>{{ k(marks.wait) }}</strong
213
+ >.</template
214
+ ><template v-else>Automatic compaction is off.</template>
215
+ </p>
216
+ </template>
217
+ </section>
218
+ <Teleport to="body">
219
+ <div v-if="detailsOpen" class="modal-backdrop" @click.self="closeDetails">
220
+ <section
221
+ class="modal compaction-modal"
222
+ role="dialog"
223
+ aria-modal="true"
224
+ aria-labelledby="compaction-title"
225
+ >
226
+ <header>
227
+ <h2 id="compaction-title">Context compaction</h2>
228
+ <button class="icon-button" aria-label="Close context compaction" @click="closeDetails">
229
+ <X :size="18" />
230
+ </button>
231
+ </header>
232
+ <p>
233
+ A summary starts in the background at <strong>{{ k(marks.auto ?? marks.wait) }}</strong
234
+ >. Above <strong>{{ k(marks.wait) }}</strong> the next request waits for it, keeping
235
+ {{ k(window - marks.wait) }} free for the answer. Drag a marker on the bar to change
236
+ either point.
237
+ </p>
238
+ <p>Older entries stay stored, and <code>search_history</code> still finds them.</p>
239
+ <label class="context-toggle">
240
+ <input
241
+ type="checkbox"
242
+ :checked="settings.enabled"
243
+ @change="save({ enabled: ($event.target as HTMLInputElement).checked })"
244
+ />
245
+ Automatic compaction
246
+ </label>
247
+ <p class="hint">
248
+ Applies to every agent in this session. <code>/compact</code> works either way.
249
+ </p>
250
+ <footer><button class="secondary" @click="closeDetails">Close</button></footer>
251
+ </section>
252
+ </div>
253
+ </Teleport>
254
+ </template>
@@ -0,0 +1,40 @@
1
+ <script setup lang="ts">
2
+ import { computed } from "vue";
3
+ import { X } from "lucide-vue-next";
4
+ import TokenManager from "./TokenManager.vue";
5
+ import { network } from "../client/store";
6
+
7
+ const props = defineProps<{ deviceId: string }>();
8
+ const emit = defineEmits<{ close: [] }>();
9
+ const device = computed(() => network.devices.value.find((d) => d.id === props.deviceId));
10
+ </script>
11
+
12
+ <template>
13
+ <div class="modal-backdrop" @click.self="emit('close')">
14
+ <section
15
+ class="modal"
16
+ role="dialog"
17
+ aria-modal="true"
18
+ aria-labelledby="device-credentials-title"
19
+ >
20
+ <header>
21
+ <h2 id="device-credentials-title">Device credentials</h2>
22
+ <button class="icon-button" aria-label="Close device credentials" @click="emit('close')">
23
+ <X :size="18" />
24
+ </button>
25
+ </header>
26
+ <template v-if="device">
27
+ <p>
28
+ {{ device.name }} <code>{{ device.id }}</code>
29
+ </p>
30
+ <p class="hint">
31
+ {{ device.status }} · tunnel {{ device.tunnel?.state ?? "disconnected" }} ·
32
+ {{ device.tunnel?.readerCount ?? 0 }} readers
33
+ </p>
34
+ <TokenManager :device="device" />
35
+ </template>
36
+ <p v-else class="hint">This device is no longer in the registry.</p>
37
+ <footer><button class="secondary" @click="emit('close')">Close</button></footer>
38
+ </section>
39
+ </div>
40
+ </template>
@@ -0,0 +1,56 @@
1
+ <script setup lang="ts">
2
+ import { computed } from "vue";
3
+ import { Laptop, Server } from "lucide-vue-next";
4
+ import type { NetworkDevice } from "@sdk/api";
5
+
6
+ // The platform a device runs on: exe.dev VMs, Macs (from DeviceInfo.platform), else a generic box.
7
+ const props = withDefaults(defineProps<{ device?: NetworkDevice; size?: number }>(), { size: 16 });
8
+ const kind = computed(() => {
9
+ const d = props.device;
10
+ if (d?.kind === "exe-dev" || d?.url?.includes(".exe.xyz")) return "exe";
11
+ if (d?.info?.platform.startsWith("darwin")) return "apple";
12
+ return d?.kind === "local" ? "laptop" : "server";
13
+ });
14
+ const label = computed(() => {
15
+ const base = { exe: "exe.dev VM", apple: "macOS", laptop: "This computer", server: "Server" }[
16
+ kind.value
17
+ ];
18
+ return props.device?.transport === "tunnel" ? `${base} (tunnel)` : base;
19
+ });
20
+ </script>
21
+
22
+ <template>
23
+ <svg
24
+ v-if="kind === 'exe'"
25
+ class="device-icon exe"
26
+ viewBox="0 0 512 512"
27
+ :width="size"
28
+ :height="size"
29
+ role="img"
30
+ :aria-label="label"
31
+ >
32
+ <title>{{ label }}</title>
33
+ <path
34
+ fill="currentColor"
35
+ fill-rule="evenodd"
36
+ d="M8 332.2l.9 1.4 1.5 .5 38.5-4.1 3.1 .4 2.7 2.2 8.8 13.2 10.1 12.8 1.1 2.6-.3 3.2-16.4 35.6 0 1.7 1 1.2 2.2 0 33.8-18.2 3.1-.8 3.4 1 12.4 8.7 15.3 9.4 1.5 2.1 .5 2.5-3.1 38.9 .4 1.5 1.3 .9 2.3-.7 25.9-29.2 2.1-1.3 2.8-.4 16.3 4.4 15.7 3.2 3 1.8 1.6 2.8 9.8 37.1 1.5 1.6 1.7-.1 1.1-1.1 14.5-35.6 1.8-2.5 3.4-1.5 15.7-1.1 16.1-2.4 3.2 .5 2.3 1.8 22.4 32 2.2 .9 1.4-.7 .6-1.5 1.4-39 1-2.7 2.4-2.2 14.7-6.8 13.8-7.8 3.4-.7 3 1 32.4 21 2.3 0 1.1-1.1 0-1.6-13.4-36.6-.3-2.5 .7-2.4 12.2-13.9 8.8-12.1 2.8-2 3.1-.3 38.4 5.4 2.1-.9 .5-1.5-.6-1.4-27.5-27.4-1.6-2.7-.2-3.1 4.6-15.8 2.9-15.8 1.3-2.6 2.5-1.9 37.3-12.2 1.6-1.9-.3-1.5-1.1-1-36.8-12.5-2.4-1.4-1.8-3.3-3.1-17.7 0-2.2 .9-2.4 3.1-2.9 10.6-15.3 3.3-1.8 3.4 .3 3.8 3 4.5 6.8 8.9 11.2 12.5 12.5 6.2 4.8 7.3 4.3 6.3 2.4 3.8 .3 4-.3 5.1-2.4 6.5-7 4.8-10.6 3.3-11.9 2-14.1 1.3-3.4 0-66.6-.8-2.6-2-13.6-3.8-14.6-7.5-18.7-4.7-9.4-4-6-7.2-7.1-5-2.1-6.5 0-2.2 .7-6 3.5-12 12-13.1 17.8-11.7 18.2-3.7 1.6-11.8-.1-3.1-1.6-1.8-2.9-.3-8.8-2.8-12.7-1.5-4-7.1-13.3-5.9-7.8-10.7-9.3-8.6-4.7-5.9-2-4.1-.5-6.7-.5-4.1 .5-7 2-7.3 3.8-7.5 7-2.2 3.2-2.5 5.5-2.8 10.5-.5 3.6 .1 17.1-.3 2.2-1.1 1.9-1.7 1.5-3.7 1.5-2.6 .3-27.5-10.5-14.5-4-15.7-3.2-2.6-1.3-1.9-3-9.9-37.5-.9-1.2-1.4-.4-1.3 .4-.8 1-14.7 36.1-1.8 2.1-2.2 1.2-17.8 1.4-15 2.2-3.4-.7-2.4-2.1-22-31.5-2-.9-1.5 .5-.7 1.5-1.4 38.6-.9 3-2.5 2.5-14.7 6.8-13.8 7.8-2.8 .7-3.1-.7-32.9-21.3-2.3 0-1.1 1.1-.1 1.6 13.6 36.9 0 3.1-1.2 2.6-10.8 12-9.5 12.9-2.8 2-3.2 .3-38.4-5.4-1.5 .4-.9 1.4 .6 2.1 27.6 27.6 1.2 1.8 .5 2.2-3 10.6-1.5 2.4-2.4 1.6-5.8 1-4.4 1.8-3.8 3-3 3.8-1.7 4-.7 4.7 .4 4.7 1.5 4.2 .3 .5 30.7 0 1.7 .6 1.4 1.1 1.1 3-1.1 2.9-2.7 1.6-31.1 .1-.6 1.1-1.2 3.6-.4 4.1 .4 4.2 1.2 3.5 2.5 4 3.2 3.1 4.1 2.3 5.6 1.8 2.4 2.1 .9 2.1 5.1 21.2-.6 2.1-1.2 1.9-28.5 26.5zM283.6 189.4l6.6 .5 5.6 1.2 6.2 2.2 5.9 3.1 4.6 3.4 4.8 4.5 3.5 4.4 3.4 5.8 2.6 6.2 1.6 6.4 .6 5.6-.2 6.7-1.1 6.5-1.7 5.4-2.9 6-3.6 5.5-7.1 7.3-8.4 5.4-9.4 3.5-9.9 1.3-11-1-40.4-10.4-2.8-1.1-3.1-2.4-1.7-.6-.9-1.1-.1-2.2-1.4-2.2 1.6-6.3 18.1-38.5 4.7-7.5 7.1-7.3 8.4-5.4 9.4-3.5zM173.9 198l-1.1 5.9-2.3 5.5-3.4 5-4.2 4.3-5 3.3-5.5 2.3-5.9 1.2-6 0-5.9-1.2-5.6-2.3-5-3.3-4.2-4.3-3.3-5-2.3-5.5-1.2-5.9 0-6 1.2-5.9 2.3-5.5 3.3-5 4.2-4.3 5-3.3 5.6-2.3 5.9-1.2 6 0 5.9 1.2 5.5 2.3 5 3.3 4.2 4.3 3.4 5 2.3 5.5 1.1 5.9zM315.1 213.5l-63.1 37.7 69.5-12.9 0-6.2-1-6.5-2.1-6.1zM237.3 260l-2.6 1.5 40.6 10.5 7.4 .9 7.4-.5 5.4-1.4 5.2-2.1 8.5-5.8 3.8-4.1 3.2-4.5 2.6-5 1.6-4.9zM284.5 196.9l-7.4 .5-8.1 2.3-7.3 4.1-6.3 5.5-5.8 8.5-17.4 37.1 3-.6 76.1-45.5-5.8-5-6.5-3.7-7.1-2.3zM141.1 183.6l-1.1-2.9-2-2.3-2.8-1.3-3.1-.1-2.9 1-2.3 2.1-1.3 2.8-.1 3 1 2.9 2.1 2.3 2.7 1.3 3.1 .2 2.9-1.1 2.3-2 1.3-2.8z"
37
+ />
38
+ </svg>
39
+ <svg
40
+ v-else-if="kind === 'apple'"
41
+ class="device-icon apple"
42
+ viewBox="0 0 384 512"
43
+ :width="size"
44
+ :height="size"
45
+ role="img"
46
+ :aria-label="label"
47
+ >
48
+ <title>{{ label }}</title>
49
+ <path
50
+ fill="currentColor"
51
+ d="M318.7 268.7c-.2-36.7 16.4-64.4 50-84.8-18.8-26.9-47.2-41.7-84.7-44.6-35.5-2.8-74.3 20.7-88.5 20.7-15 0-49.4-19.7-76.4-19.7C63.3 141.2 4 184.8 4 273.5q0 39.3 14.4 81.2c12.8 36.7 59 126.7 107.2 125.2 25.2-.6 43-17.9 75.8-17.9 31.8 0 48.3 17.9 76.4 17.9 48.6-.7 90.4-82.5 102.6-119.3-65.2-30.7-61.7-90-61.7-91.9zm-56.6-164.2c27.3-32.4 24.8-61.9 24-72.5-24.1 1.4-52 16.4-67.9 34.9-17.5 19.8-27.8 44.3-25.6 71.9 26.1 2 49.9-11.4 69.5-34.3z"
52
+ />
53
+ </svg>
54
+ <Laptop v-else-if="kind === 'laptop'" class="device-icon" :size="size" :aria-label="label" />
55
+ <Server v-else class="device-icon" :size="size" :aria-label="label" />
56
+ </template>
@@ -0,0 +1,95 @@
1
+ <script setup lang="ts">
2
+ import { ref, watch } from "vue";
3
+ import { CornerLeftUp, Folder, FolderOpen, Home } from "lucide-vue-next";
4
+ import type { DirListing } from "@sdk/api";
5
+ import { deviceClient, message } from "../client/hub";
6
+
7
+ const props = defineProps<{ deviceId: string }>();
8
+ const cwd = defineModel<string>({ required: true });
9
+ const listing = ref<DirListing>();
10
+ const loading = ref(false);
11
+ const error = ref("");
12
+
13
+ // Only the latest request may update the view: switching devices mid-load must not show the
14
+ // previous device's listing.
15
+ let seq = 0;
16
+ async function open(path: string) {
17
+ const mine = ++seq;
18
+ const deviceId = props.deviceId;
19
+ loading.value = true;
20
+ error.value = "";
21
+ try {
22
+ const result = await deviceClient(deviceId).dirs(path);
23
+ if (mine !== seq) return undefined;
24
+ listing.value = result;
25
+ if (result.path) cwd.value = result.path;
26
+ return result;
27
+ } catch (e) {
28
+ if (mine === seq) error.value = message(e);
29
+ return undefined;
30
+ } finally {
31
+ if (mine === seq) loading.value = false;
32
+ }
33
+ }
34
+
35
+ watch(
36
+ () => props.deviceId,
37
+ async () => {
38
+ listing.value = undefined;
39
+ // Start inside the first root so the list is useful right away.
40
+ const roots = await open("");
41
+ const first = roots?.dirs[0];
42
+ if (first && !roots.path) await open(first.path);
43
+ },
44
+ { immediate: true },
45
+ );
46
+ </script>
47
+
48
+ <template>
49
+ <div class="dir-browser">
50
+ <div class="dir-path">
51
+ <input
52
+ v-model="cwd"
53
+ aria-label="Working directory"
54
+ required
55
+ placeholder="/path/to/project"
56
+ spellcheck="false"
57
+ autocomplete="off"
58
+ @keydown.enter.prevent="open(cwd)"
59
+ />
60
+ <button type="button" class="secondary small" :disabled="loading || !cwd" @click="open(cwd)">
61
+ Go
62
+ </button>
63
+ </div>
64
+ <div class="dir-list" role="listbox" aria-label="Directories" :aria-busy="loading">
65
+ <button type="button" class="dir-item" title="Start directories" @click="open('')">
66
+ <Home :size="16" /><span>Start dirs</span>
67
+ </button>
68
+ <button
69
+ v-if="listing?.parent"
70
+ type="button"
71
+ class="dir-item"
72
+ :title="listing.parent"
73
+ @click="open(listing.parent)"
74
+ >
75
+ <CornerLeftUp :size="16" /><span>..</span>
76
+ </button>
77
+ <button
78
+ v-for="d in listing?.dirs"
79
+ :key="d.path"
80
+ type="button"
81
+ class="dir-item"
82
+ role="option"
83
+ :aria-selected="d.path === cwd"
84
+ :title="d.path"
85
+ @click="open(d.path)"
86
+ >
87
+ <FolderOpen v-if="d.path === cwd" :size="16" /><Folder v-else :size="16" /><span>{{
88
+ d.name
89
+ }}</span>
90
+ </button>
91
+ <p v-if="listing && !listing.dirs.length" class="hint">No subdirectories.</p>
92
+ </div>
93
+ <p v-if="error" class="message-error" role="alert">{{ error }}</p>
94
+ </div>
95
+ </template>
@@ -0,0 +1,96 @@
1
+ <script setup lang="ts">
2
+ // Custom harness extensions for the selected agent: a switch per extension, and for system-prompts a checklist of
3
+ // built-in prompts. A change edits the agent's stored selection, so the next request runs with it.
4
+ import { ref, shallowRef, watch } from "vue";
5
+ import { Puzzle } from "lucide-vue-next";
6
+ import type { AgentExtensions } from "@sdk/api";
7
+ import { agentHandle, agentState, attempt, online, refreshDetail } from "../client/store";
8
+
9
+ const data = shallowRef<AgentExtensions>();
10
+ const busy = ref(false);
11
+
12
+ async function load() {
13
+ const handle = agentHandle.value;
14
+ if (!handle || !online.value) return void (data.value = undefined);
15
+ data.value = (await handle.extensions().catch(() => undefined)) ?? data.value;
16
+ }
17
+ // Reload on a new selection, and when the agent's stored extension selection changes from any client.
18
+ watch(
19
+ [agentHandle, online, () => JSON.stringify(agentState.value.agent.extensions ?? null)],
20
+ load,
21
+ { immediate: true },
22
+ );
23
+
24
+ async function set(
25
+ patch: { name: string; enabled: boolean } | { prompt: string; enabled: boolean },
26
+ ) {
27
+ const handle = agentHandle.value;
28
+ if (!handle || busy.value) return;
29
+ busy.value = true;
30
+ try {
31
+ const next = await attempt(() => handle.setExtension(patch));
32
+ if (next) data.value = next;
33
+ void refreshDetail();
34
+ } finally {
35
+ busy.value = false;
36
+ }
37
+ }
38
+ </script>
39
+
40
+ <template>
41
+ <div class="extensions-panel">
42
+ <div class="panel-heading">
43
+ <h2>Extensions</h2>
44
+ <Puzzle :size="18" />
45
+ </div>
46
+ <p class="hint">
47
+ Custom harness capabilities for this agent. Turn one on and the next request uses it. The
48
+ choice is stored with the agent, so it survives restarts.
49
+ </p>
50
+ <p v-if="!data" class="task-empty">Loading extensions…</p>
51
+ <article
52
+ v-for="ext in data?.extensions ?? []"
53
+ :key="ext.name"
54
+ class="ext-card"
55
+ :class="{ on: ext.enabled }"
56
+ :data-extension="ext.name"
57
+ >
58
+ <div class="ext-head">
59
+ <div>
60
+ <strong>{{ ext.title }}</strong>
61
+ <code>{{ ext.name }}</code>
62
+ </div>
63
+ <button
64
+ class="switch"
65
+ role="switch"
66
+ :class="{ on: ext.enabled }"
67
+ :aria-checked="ext.enabled"
68
+ :aria-label="ext.title"
69
+ :disabled="busy"
70
+ @click="set({ name: ext.name, enabled: !ext.enabled })"
71
+ >
72
+ <span />
73
+ </button>
74
+ </div>
75
+ <p>{{ ext.description }}</p>
76
+ <ul v-if="ext.prompts" class="ext-prompts" :class="{ off: !ext.enabled }">
77
+ <li v-for="p in ext.prompts" :key="p.id">
78
+ <label>
79
+ <input
80
+ type="checkbox"
81
+ :checked="p.enabled"
82
+ :disabled="busy"
83
+ @change="set({ prompt: p.id, enabled: ($event.target as HTMLInputElement).checked })"
84
+ />
85
+ <code>{{ p.id }}</code>
86
+ </label>
87
+ <span>{{ p.description }}</span>
88
+ <details>
89
+ <summary>View prompt</summary>
90
+ <pre>{{ p.text }}</pre>
91
+ </details>
92
+ </li>
93
+ </ul>
94
+ </article>
95
+ </div>
96
+ </template>
@@ -0,0 +1,69 @@
1
+ <script setup lang="ts">
2
+ // HelpDialog: scrim + dialog presentation around the operating-model steps.
3
+ // Closes through the shared ui.dialog state; no local state to clone or lose.
4
+ import { Info, X } from "lucide-vue-next";
5
+ import { ui } from "../client/store";
6
+
7
+ const steps = [
8
+ {
9
+ title: "Pick a device, start a session",
10
+ body: "Every device in the tree runs dpi-backend. A session gets its own SQLite database on that device, in the directory you choose.",
11
+ },
12
+ {
13
+ title: "Send work, then close the browser",
14
+ body: "The device owns the run. Reopening restores committed messages, state and live progress.",
15
+ },
16
+ {
17
+ title: "Change state and fork",
18
+ body: "Edit App State by hand or through the agent. Every session and fork on a device shares it.",
19
+ },
20
+ {
21
+ title: "Interrupt deliberately",
22
+ body: "Interrupt stops foreground work and owned subagents. Background jobs continue. Stop all includes them.",
23
+ },
24
+ {
25
+ title: "Explore the boundaries",
26
+ body: "Queue a follow-up, steer after tools, wait for approval, compact context, reset or inspect raw state.",
27
+ },
28
+ {
29
+ title: "Restart a device",
30
+ body: "Its tree dims and shows the restart command. Checkpointed jobs recover on boot. Replay-safe tools may rerun. Other interrupted tools return an error.",
31
+ },
32
+ ];
33
+ </script>
34
+
35
+ <template>
36
+ <div class="scrim" @click.self="ui.dialog = undefined">
37
+ <section class="dialog" role="dialog" aria-modal="true" aria-labelledby="help-title">
38
+ <div class="dialog-head">
39
+ <div>
40
+ <span class="dialog-eyebrow">LEARN BY DOING</span>
41
+ <h3 id="help-title">What makes this durable?</h3>
42
+ </div>
43
+ <button class="icon-button" aria-label="Close help" @click="ui.dialog = undefined">
44
+ <X :size="18" />
45
+ </button>
46
+ </div>
47
+ <div class="dialog-body">
48
+ <div v-for="(step, i) in steps" :key="step.title" class="help-step">
49
+ <span class="n">{{ String(i + 1).padStart(2, "0") }}</span>
50
+ <div>
51
+ <b>{{ step.title }}</b>
52
+ <div class="body">{{ step.body }}</div>
53
+ </div>
54
+ </div>
55
+ <div class="help-callout">
56
+ <Info :size="18" /><span
57
+ >Durable does not mean exactly-once external effects. Checkpoints protect progress.
58
+ Tools still need their own idempotency.</span
59
+ >
60
+ </div>
61
+ </div>
62
+ <div class="dialog-foot">
63
+ <div class="right">
64
+ <button class="secondary" type="button" @click="ui.dialog = undefined">Close</button>
65
+ </div>
66
+ </div>
67
+ </section>
68
+ </div>
69
+ </template>