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,71 @@
1
+ <script setup lang="ts">
2
+ import { computed, ref } from "vue";
3
+ import { Check, X } from "lucide-vue-next";
4
+ import type { ModelInfo } from "@sdk/api";
5
+ import {
6
+ agentHandle,
7
+ agentState,
8
+ attempt,
9
+ device,
10
+ icon,
11
+ models,
12
+ notify,
13
+ number,
14
+ refreshDetail,
15
+ } from "../client/store";
16
+
17
+ const emit = defineEmits<{ close: [] }>();
18
+ const search = ref("");
19
+ const filtered = computed(() =>
20
+ models.value.filter((m) =>
21
+ `${m.name} ${m.id} ${m.tier}`.toLowerCase().includes(search.value.toLowerCase()),
22
+ ),
23
+ );
24
+ const isCurrent = (m: ModelInfo) =>
25
+ m.id === agentState.value.agent.model?.modelId &&
26
+ m.provider === agentState.value.agent.model?.provider;
27
+
28
+ async function choose(m: ModelInfo) {
29
+ const done = await attempt(() =>
30
+ agentHandle.value!.configure({ model: { provider: m.provider, modelId: m.id } }),
31
+ );
32
+ if (done) {
33
+ await refreshDetail();
34
+ notify("Model saved. Used on the next turn.");
35
+ emit("close");
36
+ }
37
+ }
38
+ </script>
39
+
40
+ <template>
41
+ <div class="modal-backdrop" @click.self="emit('close')">
42
+ <section class="modal model-modal">
43
+ <header>
44
+ <div>
45
+ <span class="eyebrow">PER AGENT · {{ device?.id }}</span>
46
+ <h2>Choose a model</h2>
47
+ </div>
48
+ <button class="icon-button" aria-label="Close model picker" @click="emit('close')">
49
+ <X :size="18" />
50
+ </button>
51
+ </header>
52
+ <input v-model="search" placeholder="Search models or tiers…" aria-label="Search models" />
53
+ <div class="model-list">
54
+ <button
55
+ v-for="m in filtered"
56
+ :key="`${m.provider}/${m.id}`"
57
+ :class="{ selected: isCurrent(m) }"
58
+ @click="choose(m)"
59
+ >
60
+ <img :src="icon(m.id)" alt="" />
61
+ <div>
62
+ <strong>{{ m.name }}</strong
63
+ ><small>{{ m.tier }} / {{ m.provider }} / {{ number(m.contextWindow) }} context</small>
64
+ </div>
65
+ <Check v-if="isCurrent(m)" :size="18" />
66
+ </button>
67
+ </div>
68
+ <p class="hint">Models available on {{ device?.id }}. Provider keys stay on the device.</p>
69
+ </section>
70
+ </div>
71
+ </template>
@@ -0,0 +1,236 @@
1
+ <script setup lang="ts">
2
+ import { computed, nextTick, ref } from "vue";
3
+ import { Link2, Network, PanelLeftClose, PanelLeftOpen } from "lucide-vue-next";
4
+ import DeviceIcon from "./DeviceIcon.vue";
5
+ import type { TreeNode } from "@sdk/models";
6
+ import TreeRow from "./TreeRow.vue";
7
+ import { deviceClient, hub } from "../client/hub";
8
+ import {
9
+ attempt,
10
+ expanded,
11
+ layout,
12
+ network,
13
+ notify,
14
+ select,
15
+ selected,
16
+ toggle,
17
+ ui,
18
+ panelKeys,
19
+ } from "../client/store";
20
+
21
+ interface Row {
22
+ node: TreeNode;
23
+ depth: number;
24
+ parent?: string;
25
+ }
26
+
27
+ const rows = computed(() => {
28
+ const out: Row[] = [];
29
+ const walk = (nodes: TreeNode[], depth: number, parent?: string) => {
30
+ for (const node of nodes) {
31
+ out.push({ node, depth, parent });
32
+ if (node.children.length && expanded.value.has(node.key))
33
+ walk(node.children, depth + 1, node.key);
34
+ }
35
+ };
36
+ walk(network.tree.value, 0);
37
+ return out;
38
+ });
39
+ const focusKey = ref<string>();
40
+ const list = ref<HTMLElement>();
41
+
42
+ const deviceOf = (node: TreeNode) =>
43
+ network.devices.value.find(
44
+ (d) => d.id === (node.type === "device" ? node.device.id : node.deviceId),
45
+ );
46
+ const isOnline = (node: TreeNode) => deviceOf(node)?.status === "online";
47
+ const isSelected = (node: TreeNode) =>
48
+ node.type === "agent" &&
49
+ selected.value?.deviceId === node.deviceId &&
50
+ selected.value.sessionId === node.sessionId &&
51
+ selected.value.agentId === node.agent.id;
52
+
53
+ function activate(node: TreeNode) {
54
+ focusKey.value = node.key;
55
+ if (node.type === "device") return toggle(node.key);
56
+ if (node.type === "agent")
57
+ return select({ deviceId: node.deviceId, sessionId: node.sessionId, agentId: node.agent.id });
58
+ const root = node.session.agents.find((a) => a.kind === "root") ?? node.session.agents[0];
59
+ toggle(node.key, true);
60
+ if (root) select({ deviceId: node.deviceId, sessionId: node.session.id, agentId: root.id });
61
+ }
62
+
63
+ async function action(node: TreeNode, name: string) {
64
+ const device = deviceOf(node);
65
+ if (!device) return;
66
+ const client = deviceClient(device.id);
67
+ if (node.type === "device") {
68
+ if (name === "new-session") ui.dialog = { kind: "new-session", deviceId: device.id };
69
+ else if (name === "check") await attempt(() => hub.check(device.id));
70
+ else if (name === "credentials") ui.dialog = { kind: "credentials", deviceId: device.id };
71
+ return;
72
+ }
73
+ if (node.type === "session") {
74
+ const s = node.session;
75
+ if (name === "new-agent") {
76
+ const created = await attempt(() => client.session(s.id).createAgent({}));
77
+ if (created) {
78
+ select({ deviceId: device.id, sessionId: s.id, agentId: created.id });
79
+ notify("New agent. Same session, same shared App State.");
80
+ }
81
+ } else if (name === "rename")
82
+ ui.dialog = {
83
+ kind: "prompt",
84
+ title: "Rename session",
85
+ label: "Session name",
86
+ value: s.name,
87
+ submit: "Save name",
88
+ run: (value) => client.session(s.id).rename(value),
89
+ };
90
+ else if (name === "archive")
91
+ ui.dialog = {
92
+ kind: "prompt",
93
+ title: `Archive ${s.name}?`,
94
+ hint: `Stops all of its work and moves its files to archive/ on ${device.id}. Nothing is deleted.`,
95
+ submit: "Archive session",
96
+ danger: true,
97
+ run: async () => {
98
+ await client.session(s.id).archive();
99
+ if (selected.value?.sessionId === s.id) select(undefined);
100
+ },
101
+ };
102
+ return;
103
+ }
104
+ const a = node.agent;
105
+ if (name === "rename")
106
+ ui.dialog = {
107
+ kind: "prompt",
108
+ title: "Rename agent",
109
+ label: "Agent name",
110
+ value: a.named ? a.name : "",
111
+ placeholder: a.name,
112
+ submit: "Save name",
113
+ run: (value) => client.agent(node.sessionId, a.id).rename(value),
114
+ };
115
+ }
116
+
117
+ function move(delta: number) {
118
+ const i = rows.value.findIndex((r) => r.node.key === focusKey.value);
119
+ const next = rows.value[Math.max(0, Math.min(rows.value.length - 1, (i < 0 ? 0 : i) + delta))];
120
+ if (!next) return;
121
+ focusKey.value = next.node.key;
122
+ void nextTick(() =>
123
+ list.value
124
+ ?.querySelector(`[data-key="${CSS.escape(next.node.key)}"]`)
125
+ ?.scrollIntoView({ block: "nearest" }),
126
+ );
127
+ }
128
+
129
+ function focusFirst() {
130
+ focusKey.value ??= rows.value[0]?.node.key;
131
+ }
132
+
133
+ function keydown(e: KeyboardEvent) {
134
+ const row = rows.value.find((r) => r.node.key === focusKey.value);
135
+ if (e.key === "ArrowDown") move(1);
136
+ else if (e.key === "ArrowUp") move(-1);
137
+ else if (e.key === "ArrowRight" && row) {
138
+ if (!row.node.children.length) return;
139
+ if (expanded.value.has(row.node.key)) move(1);
140
+ else toggle(row.node.key, true);
141
+ } else if (e.key === "ArrowLeft" && row) {
142
+ if (row.node.children.length && expanded.value.has(row.node.key)) toggle(row.node.key, false);
143
+ else if (row.parent) focusKey.value = row.parent;
144
+ } else if ((e.key === "Enter" || e.key === " ") && row) activate(row.node);
145
+ else return;
146
+ e.preventDefault();
147
+ }
148
+ </script>
149
+
150
+ <template>
151
+ <aside
152
+ class="network-sidebar"
153
+ :class="{ open: ui.drawer, collapsed: !layout.tree && !ui.drawer }"
154
+ aria-label="Network"
155
+ >
156
+ <template v-if="layout.tree || ui.drawer">
157
+ <header class="tree-head">
158
+ <span><Network :size="16" />NETWORK</span>
159
+ <button
160
+ class="icon-button"
161
+ aria-label="Pair a device"
162
+ title="Pair a device"
163
+ @click="ui.dialog = { kind: 'pair' }"
164
+ >
165
+ <Link2 :size="15" />
166
+ </button>
167
+ <button
168
+ class="icon-button panel-toggle"
169
+ aria-label="Collapse network"
170
+ :title="`Collapse network (${panelKeys.tree})`"
171
+ @click="((layout.tree = false), (ui.drawer = false))"
172
+ >
173
+ <PanelLeftClose :size="18" />
174
+ </button>
175
+ </header>
176
+ <div
177
+ ref="list"
178
+ class="tree-list"
179
+ role="tree"
180
+ tabindex="0"
181
+ aria-label="Devices, sessions and agents"
182
+ @keydown="keydown"
183
+ @focus="focusFirst"
184
+ >
185
+ <TreeRow
186
+ v-for="row in rows"
187
+ :key="row.node.key"
188
+ :node="row.node"
189
+ :depth="row.depth"
190
+ :open="expanded.has(row.node.key)"
191
+ :selected="isSelected(row.node)"
192
+ :focused="focusKey === row.node.key"
193
+ :online="isOnline(row.node)"
194
+ @toggle="toggle(row.node.key)"
195
+ @activate="activate(row.node)"
196
+ @action="action(row.node, $event)"
197
+ />
198
+ <p v-if="!rows.length" class="hint tree-empty" data-testid="tree-empty">
199
+ No devices yet. Declare them in <code>dpi-network.yaml</code> (start from
200
+ <code>dpi-network.example.yaml</code>), then run <code>just net up &lt;id&gt;</code>.
201
+ </p>
202
+ </div>
203
+ </template>
204
+ <div v-else class="panel-rail" aria-label="Network (collapsed)">
205
+ <button
206
+ class="icon-button panel-toggle"
207
+ aria-label="Expand network"
208
+ :title="`Expand network (${panelKeys.tree})`"
209
+ @click="layout.tree = true"
210
+ >
211
+ <PanelLeftOpen :size="18" />
212
+ </button>
213
+ <button
214
+ v-for="d in network.devices.value"
215
+ :key="d.id"
216
+ class="rail-device"
217
+ :title="`${d.id} · ${d.status}`"
218
+ :aria-label="`Show ${d.id}`"
219
+ @click="((layout.tree = true), toggle(d.id, true))"
220
+ >
221
+ <DeviceIcon :device="d" :size="20" /><i
222
+ class="dot"
223
+ :class="
224
+ d.status === 'online'
225
+ ? 'on'
226
+ : d.status === 'checking'
227
+ ? 'chk'
228
+ : d.status === 'offline'
229
+ ? 'off'
230
+ : 'err'
231
+ "
232
+ />
233
+ </button>
234
+ </div>
235
+ </aside>
236
+ </template>
@@ -0,0 +1,114 @@
1
+ <script setup lang="ts">
2
+ import { computed, ref, watch } from "vue";
3
+ import { Plus, X } from "lucide-vue-next";
4
+ import DirBrowser from "./DirBrowser.vue";
5
+ import { deviceClient, message } from "../client/hub";
6
+ import { network, notify, select } from "../client/store";
7
+
8
+ const props = defineProps<{ deviceId?: string }>();
9
+ const emit = defineEmits<{ close: [] }>();
10
+
11
+ const devices = computed(() => network.devices.value.filter((d) => d.status === "online"));
12
+ const deviceId = ref(
13
+ devices.value.find((d) => d.id === props.deviceId)?.id ?? devices.value[0]?.id ?? "",
14
+ );
15
+ const device = computed(() => devices.value.find((d) => d.id === deviceId.value));
16
+ const models = computed(() => device.value?.info?.models ?? []);
17
+ const key = (m: { provider: string; id: string }) => `${m.provider}/${m.id}`;
18
+ const draft = ref({ name: "", cwd: "", model: "" });
19
+ const pending = ref(false);
20
+ const error = ref("");
21
+
22
+ watch(
23
+ device,
24
+ (d) => {
25
+ const fallback =
26
+ d?.info?.models.find((m) => m.id === d.info?.defaultModel) ?? d?.info?.models[0];
27
+ draft.value.model = fallback ? key(fallback) : "";
28
+ draft.value.cwd = "";
29
+ },
30
+ { immediate: true },
31
+ );
32
+
33
+ async function create() {
34
+ if (pending.value || !device.value) return;
35
+ pending.value = true;
36
+ error.value = "";
37
+ try {
38
+ const model = models.value.find((m) => key(m) === draft.value.model);
39
+ if (!model) throw Error("Choose a model");
40
+ const created = await deviceClient(device.value.id).createSession({
41
+ name: draft.value.name.trim() || "New session",
42
+ cwd: draft.value.cwd.trim(),
43
+ model: { provider: model.provider, modelId: model.id },
44
+ });
45
+ const root = created.agents.find((a) => a.kind === "root") ?? created.agents[0];
46
+ select({ deviceId: device.value.id, sessionId: created.id, agentId: root?.id ?? 1 });
47
+ notify(`Session created on ${device.value.id}.`);
48
+ emit("close");
49
+ } catch (e) {
50
+ error.value = message(e);
51
+ } finally {
52
+ pending.value = false;
53
+ }
54
+ }
55
+ </script>
56
+
57
+ <template>
58
+ <div class="modal-backdrop" @click.self="emit('close')">
59
+ <form class="modal session-modal" @submit.prevent="create">
60
+ <header>
61
+ <div>
62
+ <span class="eyebrow">A PERSISTENT WORKSPACE</span>
63
+ <h2>New session</h2>
64
+ </div>
65
+ <button
66
+ type="button"
67
+ class="icon-button"
68
+ aria-label="Close new session"
69
+ @click="emit('close')"
70
+ >
71
+ <X :size="18" />
72
+ </button>
73
+ </header>
74
+ <p v-if="!devices.length" class="message-error">
75
+ No device is online. Start one with just backend run or just net up &lt;id&gt;, then try
76
+ again.
77
+ </p>
78
+ <template v-else>
79
+ <label
80
+ >Device<select v-model="deviceId" aria-label="Device">
81
+ <option v-for="d in devices" :key="d.id" :value="d.id">
82
+ {{ d.id }} · {{ d.info?.hostname ?? d.url }}
83
+ </option>
84
+ </select></label
85
+ >
86
+ <label
87
+ >Session name<input
88
+ v-model="draft.name"
89
+ autofocus
90
+ placeholder="My durable agent"
91
+ maxlength="100"
92
+ /></label>
93
+ <div class="field">
94
+ <span>Working directory on {{ deviceId }}</span>
95
+ <DirBrowser v-model="draft.cwd" :device-id="deviceId" />
96
+ </div>
97
+ <label
98
+ >Model<select v-model="draft.model" aria-label="New session model">
99
+ <option v-for="m in models" :key="key(m)" :value="key(m)">
100
+ {{ m.tier }} / {{ m.name }}
101
+ </option>
102
+ </select></label
103
+ >
104
+ </template>
105
+ <p v-if="error" class="message-error" role="alert">{{ error }}</p>
106
+ <footer>
107
+ <button type="button" class="secondary" @click="emit('close')">Cancel</button
108
+ ><button class="primary" :disabled="pending || !draft.model || !draft.cwd || !device">
109
+ <Plus :size="18" />{{ pending ? "Creating…" : "Create session" }}
110
+ </button>
111
+ </footer>
112
+ </form>
113
+ </div>
114
+ </template>
@@ -0,0 +1,128 @@
1
+ <script setup lang="ts">
2
+ import { computed, ref } from "vue";
3
+ import { Copy, RefreshCw, ShieldAlert, WifiOff, Loader } from "lucide-vue-next";
4
+ import type { NetworkDevice } from "@sdk/api";
5
+ import { hub, message } from "../client/hub";
6
+ import { ago, notify } from "../client/store";
7
+
8
+ const props = defineProps<{ device: NetworkDevice }>();
9
+ const busy = ref(false);
10
+ const error = ref("");
11
+
12
+ /** The exact fix for this device's state. Device setup happens in the terminal, never in this UI. */
13
+ const fix = computed(() => {
14
+ const d = props.device;
15
+ const config = d.source === "config";
16
+ switch (d.status) {
17
+ case "checking":
18
+ return {
19
+ title: `Checking ${d.id}…`,
20
+ text: "Probing the device. This view fills in as soon as it answers.",
21
+ };
22
+ case "unauthorized":
23
+ if (d.transport === "tunnel") {
24
+ // The hub never parks tunnel devices on unauthorized; if it happens,
25
+ // point at pairing, not at a token the browser does not hold.
26
+ return {
27
+ title: `${d.id} tunnel credentials are not valid`,
28
+ text: "Pair the device again — the hub will mint a fresh credential.",
29
+ };
30
+ }
31
+ return {
32
+ title: d.hasToken ? `${d.id} rejected its token` : `No token for ${d.id}`,
33
+ text: d.hasToken
34
+ ? "The device's DPI_TOKEN changed (a rotate or a rebuilt device). Copy the current token into this hub's registry, then check again."
35
+ : "This hub has no token for the device yet. Register it, then check again.",
36
+ command:
37
+ config && d.kind !== "url"
38
+ ? `just net register ${d.id}`
39
+ : `just net register ${d.id} --token <token>`,
40
+ };
41
+ case "mismatch":
42
+ return {
43
+ title: `${d.id} answers as a different device`,
44
+ text: "The URL now reaches a backend with another identity (a rebuilt VM or a new data directory). If that is expected, register it again: that trusts the identity it answers with now.",
45
+ command:
46
+ d.kind === "url"
47
+ ? `just net register ${d.id} --token <token>`
48
+ : `just net register ${d.id}`,
49
+ };
50
+ case "incompatible":
51
+ return {
52
+ title: `${d.id} speaks another API version`,
53
+ text: "Update the device's dpi-backend.",
54
+ command: config ? `just net deploy ${d.id}` : undefined,
55
+ };
56
+ default:
57
+ return {
58
+ title: `${d.id} is offline`,
59
+ text: `Last seen ${ago(d.lastSeenAt)}. Its agents keep their state on the device and resume when its backend starts again. The tree shows the last known sessions.`,
60
+ command: !config
61
+ ? undefined
62
+ : d.kind === "local"
63
+ ? "just backend run"
64
+ : d.kind === "url"
65
+ ? undefined
66
+ : `just net restart ${d.id}`,
67
+ alt: config && d.kind !== "local" && d.kind !== "url" ? `just net up ${d.id}` : undefined,
68
+ };
69
+ }
70
+ });
71
+
72
+ async function check() {
73
+ busy.value = true;
74
+ error.value = "";
75
+ try {
76
+ const d = await hub.check(props.device.id);
77
+ if (d.status === "online") notify(`${d.id} is online.`);
78
+ } catch (e) {
79
+ error.value = message(e);
80
+ } finally {
81
+ busy.value = false;
82
+ }
83
+ }
84
+ async function copy(text: string) {
85
+ try {
86
+ await navigator.clipboard.writeText(text);
87
+ notify("Copied.");
88
+ } catch {
89
+ notify("Clipboard needs localhost or HTTPS. Select the text to copy.");
90
+ }
91
+ }
92
+ </script>
93
+
94
+ <template>
95
+ <section class="chat-panel offline-panel" :class="device.status" aria-live="polite">
96
+ <div class="chat-empty">
97
+ <Loader v-if="device.status === 'checking'" class="spin" :size="30" />
98
+ <ShieldAlert
99
+ v-else-if="device.status === 'mismatch' || device.status === 'unauthorized'"
100
+ :size="30"
101
+ />
102
+ <WifiOff v-else :size="30" />
103
+ <span class="eyebrow"
104
+ >{{ device.kind.toUpperCase() }} ·
105
+ {{ device.url ?? (device.transport === "tunnel" ? "tunnel" : "no endpoint") }}</span
106
+ >
107
+ <h2>{{ fix.title }}</h2>
108
+ <p>{{ fix.text }}</p>
109
+ <p v-if="device.error" class="hint offline-error">{{ device.error }}</p>
110
+ <div v-if="fix.command" class="offline-command">
111
+ <code>{{ fix.command }}</code>
112
+ <button class="icon-button" aria-label="Copy command" @click="copy(fix.command)">
113
+ <Copy :size="18" />
114
+ </button>
115
+ </div>
116
+ <p v-if="fix.command" class="hint">Run it in this repo's terminal, then press Check now.</p>
117
+ <p v-if="fix.alt" class="hint">
118
+ Never deployed or rebuilt? <code>{{ fix.alt }}</code> provisions it from scratch.
119
+ </p>
120
+ <div class="offline-actions">
121
+ <button class="secondary" :disabled="busy" @click="check()">
122
+ <RefreshCw :size="18" />{{ busy ? "Checking…" : "Check now" }}
123
+ </button>
124
+ </div>
125
+ <p v-if="error" class="message-error" role="alert">{{ error }}</p>
126
+ </div>
127
+ </section>
128
+ </template>