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,425 @@
1
+ <script setup lang="ts">
2
+ import { computed, ref, watch } from "vue";
3
+ import {
4
+ Activity,
5
+ Braces,
6
+ ChevronDown,
7
+ FolderOpen,
8
+ Pencil,
9
+ Server,
10
+ ShieldCheck,
11
+ Wrench,
12
+ PanelLeftClose,
13
+ PanelLeftOpen,
14
+ Puzzle,
15
+ } from "lucide-vue-next";
16
+ import { THINKING_LEVELS, type ThinkingLevel } from "@sdk/api";
17
+ import type { StateChange } from "@sdk/app-state";
18
+ import StateEditor from "./StateEditor.vue";
19
+ import TunnelStatusBar from "./TunnelStatusBar.vue";
20
+ import TokenManager from "./TokenManager.vue";
21
+ import TasksPanel from "./TasksPanel.vue";
22
+ import ExtensionsPanel from "./ExtensionsPanel.vue";
23
+ import ContextBar from "./ContextBar.vue";
24
+ import { message } from "../client/hub";
25
+ import { toolSignature } from "../client/signature";
26
+ import {
27
+ agentHandle,
28
+ agentState,
29
+ attempt,
30
+ busy,
31
+ currentModel,
32
+ detail,
33
+ device,
34
+ deviceFeed,
35
+ hideInspector,
36
+ icon,
37
+ inspectorExpanded,
38
+ json,
39
+ notify,
40
+ number,
41
+ refreshDetail,
42
+ session,
43
+ sessionHandle,
44
+ sessionSummary,
45
+ showInspector,
46
+ tasks,
47
+ ui,
48
+ usage,
49
+ panelKeys,
50
+ } from "../client/store";
51
+
52
+ // Each tool's parameters as a highlighted TypeScript declaration, rebuilt only when the tools change.
53
+ const signatures = computed(
54
+ () => new Map(detail.value?.tools.map((t) => [t.name, toolSignature(t.name, t.parameters)])),
55
+ );
56
+
57
+ const tabs = [
58
+ { id: "session", label: "Session", icon: FolderOpen },
59
+ { id: "state", label: "State", icon: Braces },
60
+ { id: "tasks", label: "Tasks", icon: Activity },
61
+ { id: "tools", label: "Tools", icon: Wrench },
62
+ { id: "extensions", label: "Extend", icon: Puzzle },
63
+ ];
64
+ const usageRows = computed(() => [
65
+ { label: "Input", tokens: usage.value.input, cost: usage.value.inputCost },
66
+ { label: "Output", tokens: usage.value.output, cost: usage.value.outputCost },
67
+ { label: "Cache read", tokens: usage.value.cacheRead, cost: usage.value.cacheReadCost },
68
+ { label: "Cache write", tokens: usage.value.cacheWrite, cost: usage.value.cacheWriteCost },
69
+ ]);
70
+ const background = computed(() => tasks.value.filter((t) => t.background).length);
71
+
72
+ function configure(
73
+ patch: Parameters<NonNullable<typeof agentHandle.value>["configure"]>[0],
74
+ done?: string,
75
+ ) {
76
+ void attempt(async () => {
77
+ await agentHandle.value!.configure(patch);
78
+ await refreshDetail();
79
+ if (done) notify(done);
80
+ });
81
+ }
82
+
83
+ function renameSession() {
84
+ const handle = sessionHandle.value;
85
+ if (!handle) return;
86
+ ui.dialog = {
87
+ kind: "prompt",
88
+ title: "Rename session",
89
+ label: "Session name",
90
+ value: sessionSummary.value?.name ?? "",
91
+ submit: "Save name",
92
+ run: (name) => handle.rename(name),
93
+ };
94
+ }
95
+
96
+ // ------------------------------------------------------------ App State (device-wide document)
97
+
98
+ const shared = deviceFeed.sharedState;
99
+ const stateDraft = ref("");
100
+ const stateBaseline = ref("");
101
+ const stateDraftRevision = ref(0);
102
+ const stateSaving = ref(false);
103
+ const stateError = ref("");
104
+ const stateDirty = computed(() => stateDraft.value !== stateBaseline.value);
105
+ const stateConflict = computed(
106
+ () => stateDirty.value && shared.value?.revision !== stateDraftRevision.value,
107
+ );
108
+ function resetStateDraft() {
109
+ stateDraft.value = json(shared.value?.value ?? {});
110
+ stateBaseline.value = stateDraft.value;
111
+ stateDraftRevision.value = shared.value?.revision ?? 0;
112
+ stateError.value = "";
113
+ }
114
+ watch(shared, () => !stateDirty.value && resetStateDraft(), { immediate: true });
115
+
116
+ async function editState(change: StateChange) {
117
+ if (stateSaving.value || !agentHandle.value) return;
118
+ stateSaving.value = true;
119
+ stateError.value = "";
120
+ try {
121
+ await agentHandle.value.demo("state", change);
122
+ } catch (e) {
123
+ stateError.value = message(e);
124
+ } finally {
125
+ stateSaving.value = false;
126
+ }
127
+ }
128
+ async function saveStateJson() {
129
+ if (stateSaving.value || !agentHandle.value) return;
130
+ stateError.value = "";
131
+ stateSaving.value = true;
132
+ try {
133
+ const value = JSON.parse(stateDraft.value);
134
+ if (!value || Array.isArray(value) || typeof value !== "object")
135
+ throw Error("App State must be a JSON object.");
136
+ const saved = await agentHandle.value.demo<{ value: unknown; revision: number }>("state", {
137
+ operation: "update",
138
+ path: "",
139
+ value,
140
+ expectedRevision: stateDraftRevision.value,
141
+ });
142
+ stateDraft.value = json(saved.value);
143
+ stateBaseline.value = stateDraft.value;
144
+ stateDraftRevision.value = saved.revision;
145
+ notify("Shared App State saved.");
146
+ } catch (e) {
147
+ stateError.value = message(e);
148
+ } finally {
149
+ stateSaving.value = false;
150
+ }
151
+ }
152
+
153
+ const readOnly = computed(() => detail.value?.config.readOnly === true);
154
+ const offeredTools = computed(() => detail.value?.tools.filter((t) => t.offered).length ?? 0);
155
+ </script>
156
+
157
+ <template>
158
+ <aside class="inspector" :class="{ collapsed: !inspectorExpanded, overlay: ui.inspectorOverlay }">
159
+ <div v-if="!inspectorExpanded" class="panel-rail" aria-label="Inspector (collapsed)">
160
+ <button
161
+ class="icon-button panel-toggle"
162
+ aria-label="Expand inspector"
163
+ :title="`Expand inspector (${panelKeys.inspector})`"
164
+ @click="showInspector()"
165
+ >
166
+ <PanelLeftOpen :size="18" />
167
+ </button>
168
+ <button
169
+ v-for="t in tabs"
170
+ :key="t.id"
171
+ class="rail-tab"
172
+ :class="{ active: ui.tab === t.id }"
173
+ :aria-label="`Open ${t.label}`"
174
+ :title="t.label"
175
+ @click="showInspector(t.id)"
176
+ >
177
+ <component :is="t.icon" :size="20" /><span
178
+ v-if="t.id === 'tasks' && tasks.length"
179
+ class="count"
180
+ >{{ tasks.length }}</span
181
+ >
182
+ </button>
183
+ </div>
184
+ <template v-else>
185
+ <div class="inspector-tabs" role="tablist" aria-label="Session details">
186
+ <button
187
+ v-for="t in tabs"
188
+ :id="`inspector-tab-${t.id}`"
189
+ :key="t.id"
190
+ role="tab"
191
+ aria-controls="inspector-panel"
192
+ :aria-selected="ui.tab === t.id"
193
+ :class="{ active: ui.tab === t.id }"
194
+ @click="ui.tab = t.id"
195
+ >
196
+ <component :is="t.icon" :size="18" />{{ t.label
197
+ }}<span v-if="t.id === 'tasks' && tasks.length" class="count">{{ tasks.length }}</span>
198
+ </button>
199
+ <button
200
+ class="icon-button panel-toggle"
201
+ aria-label="Collapse inspector"
202
+ :title="`Collapse inspector (${panelKeys.inspector})`"
203
+ @click="hideInspector()"
204
+ >
205
+ <PanelLeftClose :size="18" />
206
+ </button>
207
+ </div>
208
+ <div
209
+ id="inspector-panel"
210
+ class="inspector-body"
211
+ :class="{ 'session-panel': ui.tab === 'session' }"
212
+ role="tabpanel"
213
+ :aria-labelledby="`inspector-tab-${ui.tab}`"
214
+ >
215
+ <template v-if="ui.tab === 'session'">
216
+ <section class="session-heading">
217
+ <h1>
218
+ <span>{{ sessionSummary?.name ?? "Loading session" }}</span
219
+ ><button
220
+ class="icon-button"
221
+ title="Rename session"
222
+ aria-label="Rename session"
223
+ @click="renameSession"
224
+ >
225
+ <Pencil :size="18" />
226
+ </button>
227
+ </h1>
228
+ <button
229
+ class="path cwd-button"
230
+ :title="sessionSummary?.cwd"
231
+ aria-label="Show full working directory"
232
+ @click="ui.dialog = { kind: 'path', path: sessionSummary?.cwd ?? '' }"
233
+ >
234
+ <FolderOpen :size="18" /><span class="cwd-trail" dir="rtl"
235
+ ><bdi dir="ltr">{{ sessionSummary?.cwd }}</bdi></span
236
+ >
237
+ </button>
238
+ <div class="session-status">
239
+ <span class="status-pill" :class="{ working: busy }"
240
+ ><i />{{ busy ? "Working" : "Idle" }}</span
241
+ ><span v-if="background" class="background-pill">{{ background }} background</span
242
+ ><span class="saved"><Server :size="18" />{{ device?.id }}</span>
243
+ <TunnelStatusBar v-if="device" :device="device" />
244
+ </div>
245
+ </section>
246
+ <TokenManager v-if="device" :device="device" />
247
+ <section class="model-section">
248
+ <div class="section-label">
249
+ <span>AGENT</span>
250
+ </div>
251
+ <button class="model-button" @click="ui.dialog = { kind: 'models' }">
252
+ <img :src="icon(agentState.agent.model?.modelId)" alt="" />
253
+ <div>
254
+ <strong>{{
255
+ currentModel?.name ?? agentState.agent.model?.modelId ?? "Loading…"
256
+ }}</strong
257
+ ><small
258
+ >{{ currentModel?.tier ?? "" }} /
259
+ {{ agentState.agent.model?.provider ?? "" }}</small
260
+ >
261
+ </div>
262
+ <ChevronDown :size="18" />
263
+ </button>
264
+ <div class="thinking-row">
265
+ <span>Thinking</span
266
+ ><select
267
+ aria-label="Thinking level"
268
+ :value="agentState.agent.thinkingLevel ?? 'low'"
269
+ @change="
270
+ configure(
271
+ { thinkingLevel: ($event.target as HTMLSelectElement).value as ThinkingLevel },
272
+ `Thinking ${($event.target as HTMLSelectElement).value}. Used on the next turn.`,
273
+ )
274
+ "
275
+ >
276
+ <option v-for="level in THINKING_LEVELS" :key="level">{{ level }}</option>
277
+ </select>
278
+ </div>
279
+ <ContextBar />
280
+ <section class="stats-section" aria-labelledby="stats-title">
281
+ <div class="section-label"><span id="stats-title">STATS</span></div>
282
+ <div class="metrics">
283
+ <div :title="`${usage.tokens.toLocaleString('en-US')} total tokens`">
284
+ <span>{{ number(usage.tokens) }}</span
285
+ ><small>total tokens</small>
286
+ </div>
287
+ <div>
288
+ <span>${{ usage.cost.toFixed(4) }}</span
289
+ ><small>agent cost</small>
290
+ </div>
291
+ <div>
292
+ <span>{{ detail?.tools.length ?? 0 }}</span
293
+ ><small>tools</small>
294
+ </div>
295
+ </div>
296
+ <table class="usage-breakdown" aria-label="Agent token and cost breakdown">
297
+ <thead>
298
+ <tr>
299
+ <th scope="col">Type</th>
300
+ <th scope="col">Tokens</th>
301
+ <th scope="col">Cost (USD)</th>
302
+ </tr>
303
+ </thead>
304
+ <tbody>
305
+ <tr v-for="row in usageRows" :key="row.label">
306
+ <th scope="row">{{ row.label }}</th>
307
+ <td>{{ row.tokens.toLocaleString("en-US") }}</td>
308
+ <td>${{ row.cost.toFixed(4) }}</td>
309
+ </tr>
310
+ </tbody>
311
+ </table>
312
+ </section>
313
+ </section>
314
+ </template>
315
+ <template v-if="ui.tab === 'state'">
316
+ <div class="panel-heading">
317
+ <h2>App State</h2>
318
+ <Braces :size="18" />
319
+ </div>
320
+ <p class="hint app-state-description">
321
+ One shared JSON object for every agent, session and fork on {{ device?.id }}. You and
322
+ your agents can read, create, update or delete values and fields. Changes appear live
323
+ and persist across closed browsers, interrupted agents and backend restarts.
324
+ </p>
325
+ <textarea
326
+ v-model="stateDraft"
327
+ class="app-state-json"
328
+ aria-label="App State JSON"
329
+ spellcheck="false"
330
+ :disabled="stateSaving || !shared"
331
+ rows="12"
332
+ />
333
+ <div class="state-json-actions">
334
+ <span v-if="stateConflict" class="hint"
335
+ >State changed elsewhere. Load the latest JSON before saving.</span
336
+ >
337
+ <button
338
+ v-if="stateDirty"
339
+ class="secondary"
340
+ :disabled="stateSaving"
341
+ @click="resetStateDraft"
342
+ >
343
+ {{ stateConflict ? "Load latest" : "Reset" }}
344
+ </button>
345
+ <button
346
+ class="primary small"
347
+ :disabled="!stateDirty || stateSaving || stateConflict"
348
+ @click="saveStateJson"
349
+ >
350
+ {{ stateSaving ? "Saving…" : "Save JSON" }}
351
+ </button>
352
+ </div>
353
+ <p v-if="stateError" class="message-error" role="alert">{{ stateError }}</p>
354
+ <StateEditor
355
+ :value="shared?.value ?? {}"
356
+ :disabled="stateSaving || stateDirty || !shared"
357
+ @change="editState"
358
+ />
359
+ </template>
360
+ <TasksPanel v-if="ui.tab === 'tasks'" />
361
+ <ExtensionsPanel v-if="ui.tab === 'extensions'" />
362
+ <template v-if="ui.tab === 'tools'"
363
+ ><div class="panel-heading">
364
+ <h2>Tools</h2>
365
+ <span class="tag">{{ offeredTools }} / {{ detail?.tools.length ?? 0 }}</span>
366
+ </div>
367
+ <p class="hint">
368
+ Every tool this agent's extensions provide. Open one to see its definition.
369
+ </p>
370
+ <div class="hook-card">
371
+ <div>
372
+ <ShieldCheck :size="18" /><strong>Read only</strong
373
+ ><button
374
+ class="switch"
375
+ role="switch"
376
+ :aria-checked="readOnly"
377
+ aria-label="Read only"
378
+ :class="{ on: readOnly }"
379
+ @click="
380
+ configure(
381
+ { readOnly: !readOnly },
382
+ readOnly
383
+ ? 'Read only off. Every tool is back.'
384
+ : 'Read only on. Tools that write are disabled.',
385
+ )
386
+ "
387
+ >
388
+ <span />
389
+ </button>
390
+ </div>
391
+ <p>
392
+ Offers this agent only tools that read. Tools that create, update or delete leave its
393
+ next request, and the choice is stored with the agent.
394
+ </p>
395
+ </div>
396
+ <details
397
+ v-for="tool in detail?.tools"
398
+ :key="tool.name"
399
+ class="tool-row"
400
+ :class="{ disabled: !tool.offered }"
401
+ >
402
+ <summary>
403
+ <Wrench :size="18" /><strong>{{ tool.name }}</strong
404
+ ><span v-if="!tool.offered" class="tag tool-disabled">disabled</span
405
+ ><span class="tag">{{
406
+ tool.replay === "safe" ? "replay safe" : "interrupt on crash"
407
+ }}</span>
408
+ </summary>
409
+ <p>{{ tool.description }}</p>
410
+ <pre
411
+ v-if="tool.parameters"
412
+ class="tool-signature"
413
+ :aria-label="`${tool.name} TypeScript definition`"
414
+ ><code><span v-for="(t, i) in signatures.get(tool.name)" :key="i" :class="`tk-${t.kind}`">{{ t.text }}</span></code></pre>
415
+ <small>{{
416
+ tool.replay === "safe"
417
+ ? "An interrupted execution may run again after recovery."
418
+ : "A crash records an interrupted result instead of rerunning this effect."
419
+ }}</small>
420
+ </details></template
421
+ >
422
+ </div>
423
+ </template>
424
+ </aside>
425
+ </template>
@@ -0,0 +1,221 @@
1
+ <script setup lang="ts">
2
+ // One durable job run, live: its named phases as a stepper, every step each phase took (commands, a subagent's
3
+ // tool calls, App State writes, checkpoints), the approval when it waits, and how it is owned.
4
+ import { computed, h, type FunctionalComponent } from "vue";
5
+ import {
6
+ Bot,
7
+ Braces,
8
+ Check,
9
+ Flag,
10
+ Hand,
11
+ ListChecks,
12
+ LoaderCircle,
13
+ Octagon,
14
+ Repeat,
15
+ ShieldCheck,
16
+ Terminal,
17
+ Workflow,
18
+ Wrench,
19
+ X,
20
+ } from "lucide-vue-next";
21
+ import { jobRunDone, type JobPhaseRun, type JobRun, type JobStep } from "@sdk/jobs";
22
+ import { decide, jobDef, now, openAgent, pending, seconds, stopRun } from "../client/jobs";
23
+
24
+ const props = defineProps<{ run: JobRun }>();
25
+ const def = computed(() => jobDef(props.run.job));
26
+ const done = computed(() => jobRunDone(props.run));
27
+ const elapsed = computed(() => seconds((props.run.endedAt ?? now.value) - props.run.startedAt));
28
+ const statusText: Record<JobRun["status"], string> = {
29
+ running: "Running",
30
+ waiting: "Waiting for you",
31
+ completed: "Completed",
32
+ failed: "Failed",
33
+ aborted: "Stopped",
34
+ declined: "Declined",
35
+ };
36
+ const stepIcon = {
37
+ command: Terminal,
38
+ tool: Wrench,
39
+ agent: Bot,
40
+ state: Braces,
41
+ approval: ShieldCheck,
42
+ checkpoint: Flag,
43
+ note: ListChecks,
44
+ };
45
+ const stepTitle = (s: JobStep) =>
46
+ s.kind === "tool" ? `${s.label}: a subagent tool call, its own durable task` : s.label;
47
+
48
+ /** One step: what kind, what ran, how it ended, how long it took. */
49
+ const JobStepLine: FunctionalComponent<{ step: JobStep }> = (p) => {
50
+ const s = p.step;
51
+ const label = ["command", "tool", "state"].includes(s.kind)
52
+ ? h("code", { title: stepTitle(s) }, s.label)
53
+ : h("span", { class: "job-step-text" }, s.label);
54
+ const state =
55
+ s.status === "running"
56
+ ? h(LoaderCircle, { size: 15, class: "spin" })
57
+ : s.status === "done"
58
+ ? h(Check, { size: 15 })
59
+ : s.status === "waiting"
60
+ ? h(Hand, { size: 15 })
61
+ : h(X, { size: 15 });
62
+ return [
63
+ h(stepIcon[s.kind], { size: 16, class: "job-step-kind" }),
64
+ label,
65
+ s.kind === "tool" && s.detail
66
+ ? h("span", { class: "job-step-args", title: s.detail }, s.detail)
67
+ : null,
68
+ h("span", { class: "job-step-state" }, [
69
+ state,
70
+ s.ms !== undefined && s.kind !== "note" ? h("span", seconds(s.ms)) : null,
71
+ ]),
72
+ ];
73
+ };
74
+ JobStepLine.props = ["step"];
75
+ const phaseSpec = (id: string) => def.value?.phases.find((p) => p.id === id);
76
+ const phaseTime = (p: JobPhaseRun) =>
77
+ p.startedAt
78
+ ? seconds(
79
+ (p.endedAt ??
80
+ (p.status === "running" || p.status === "waiting" ? now.value : p.startedAt)) -
81
+ p.startedAt,
82
+ )
83
+ : "";
84
+ const open = (p: JobPhaseRun) => p.status !== "pending" && p.status !== "skipped";
85
+ const inputs = computed(() => Object.entries(props.run.input));
86
+ const approvalPhase = computed(() => props.run.phases.find((p) => p.status === "waiting"));
87
+ </script>
88
+
89
+ <template>
90
+ <article
91
+ class="job-run"
92
+ :class="`is-${run.status}`"
93
+ :data-job-run="run.id"
94
+ :aria-label="`${run.title} run ${run.run}: ${statusText[run.status]}`"
95
+ >
96
+ <header class="job-run-head">
97
+ <span class="job-run-icon"><Workflow :size="22" /></span>
98
+ <div class="job-run-titles">
99
+ <div class="job-run-eyebrow">
100
+ <span>DURABLE JOB</span>
101
+ <span class="job-run-repeat"
102
+ ><Repeat :size="15" />Repeatable workflow, run #{{ run.run }}</span
103
+ >
104
+ </div>
105
+ <h3>{{ run.title }}</h3>
106
+ </div>
107
+ <span class="job-run-status" :class="run.status"><i />{{ statusText[run.status] }}</span>
108
+ </header>
109
+ <p v-if="def" class="job-run-about">{{ def.summary }}</p>
110
+ <div class="job-run-meta">
111
+ <span>task #{{ run.id }}</span>
112
+ <span>{{ run.startedBy === "agent" ? "started by the agent" : "started by you" }}</span>
113
+ <span>{{ elapsed }}</span>
114
+ <span v-for="[k, v] in inputs" :key="k" class="job-run-input">{{ k }}={{ v }}</span>
115
+ <span v-if="run.resumed" class="job-run-resumed"
116
+ >resumed {{ run.resumed }}× after restart</span
117
+ >
118
+ </div>
119
+
120
+ <ol class="job-stepper" aria-label="Phases">
121
+ <li
122
+ v-for="(phase, i) in run.phases"
123
+ :key="phase.id"
124
+ :class="`ph-${phase.status}`"
125
+ :aria-current="run.phase === phase.id && !done ? 'step' : undefined"
126
+ >
127
+ <span class="job-chip"
128
+ ><span class="job-step-dot">
129
+ <Check v-if="phase.status === 'done'" :size="15" />
130
+ <LoaderCircle v-else-if="phase.status === 'running'" :size="15" class="spin" />
131
+ <Hand v-else-if="phase.status === 'waiting'" :size="15" />
132
+ <X v-else-if="['failed', 'aborted'].includes(phase.status)" :size="15" />
133
+ <template v-else>{{ i + 1 }}</template>
134
+ </span>
135
+ <span class="job-step-label">{{ phase.label }}</span>
136
+ <span v-if="phase.progress" class="job-step-progress"
137
+ >{{ phase.progress.done }}/{{ phase.progress.total }}</span
138
+ ></span
139
+ >
140
+ </li>
141
+ </ol>
142
+
143
+ <div class="job-phases">
144
+ <details
145
+ v-for="(phase, i) in run.phases"
146
+ :key="phase.id"
147
+ class="job-phase"
148
+ :class="`ph-${phase.status}`"
149
+ :open="open(phase)"
150
+ >
151
+ <summary>
152
+ <span class="job-phase-n">{{ i + 1 }}</span>
153
+ <strong>{{ phase.label }}</strong>
154
+ <span class="job-phase-summary">{{
155
+ phase.summary ??
156
+ (phase.status === "pending"
157
+ ? phaseSpec(phase.id)?.summary
158
+ : phase.status === "skipped"
159
+ ? "Skipped"
160
+ : "")
161
+ }}</span>
162
+ <span class="job-phase-time">{{ phaseTime(phase) }}</span>
163
+ </summary>
164
+ <p v-if="phase.status !== 'done'" class="job-phase-about">
165
+ {{ phaseSpec(phase.id)?.summary }}
166
+ </p>
167
+ <ul v-if="phase.steps.length" class="job-steps">
168
+ <li
169
+ v-for="step in phase.steps"
170
+ :key="step.id"
171
+ :class="[`k-${step.kind}`, `st-${step.status}`]"
172
+ >
173
+ <details v-if="step.detail && step.kind !== 'tool'" :open="step.status === 'error'">
174
+ <summary><JobStepLine :step="step" /></summary>
175
+ <pre>{{ step.detail }}</pre>
176
+ </details>
177
+ <div v-else class="job-step-line"><JobStepLine :step="step" /></div>
178
+ <button
179
+ v-if="step.kind === 'agent' && step.conversationId"
180
+ class="text-button job-step-link"
181
+ @click="openAgent(step.conversationId)"
182
+ >
183
+ Open subagent #{{ step.conversationId }}
184
+ </button>
185
+ </li>
186
+ </ul>
187
+ <p v-else-if="phase.status === 'pending'" class="job-phase-empty">Not started yet.</p>
188
+ </details>
189
+ </div>
190
+
191
+ <div v-if="run.approval === 'pending'" class="job-approval" role="group" aria-label="Approval">
192
+ <div><ShieldCheck :size="20" /><strong>Approve to continue</strong></div>
193
+ <p>
194
+ {{ approvalPhase ? phaseSpec(approvalPhase.id)?.summary : "This run waits for you" }}. It is
195
+ paused on a saved checkpoint: close the browser or restart the device and the decision will
196
+ still be waiting.
197
+ </p>
198
+ <div class="job-approval-buttons">
199
+ <button class="primary small" :disabled="!!pending" @click="decide(run, true)">
200
+ <Check :size="18" />Approve &amp; continue
201
+ </button>
202
+ <button class="secondary" :disabled="!!pending" @click="decide(run, false)">Decline</button>
203
+ </div>
204
+ </div>
205
+ <p v-if="run.error && run.status !== 'aborted'" class="message-error">{{ run.error }}</p>
206
+
207
+ <footer class="job-run-foot">
208
+ <span
209
+ >{{
210
+ run.background
211
+ ? "Runs in the background: the conversation owns it, so Interrupt leaves it running."
212
+ : "Owned by this tool call: Interrupt stops it."
213
+ }}
214
+ Each phase saves a checkpoint, so a restart resumes the current phase.</span
215
+ >
216
+ <button v-if="!done" class="secondary danger" :disabled="!!pending" @click="stopRun(run)">
217
+ <Octagon :size="16" />Stop
218
+ </button>
219
+ </footer>
220
+ </article>
221
+ </template>
@@ -0,0 +1,39 @@
1
+ <script setup lang="ts">
2
+ // MobileNav: three-view phone nav (chat | details | network) mapped onto the existing
3
+ // store state instead of a new mview field — net = the tree drawer, details = the inspector
4
+ // overlay, chat = neither. The store keeps the overlays mutually exclusive.
5
+ import { MessageSquare, PanelRight, Waypoints } from "lucide-vue-next";
6
+ import {
7
+ inspectorExpanded,
8
+ tasks,
9
+ ui,
10
+ hideInspector,
11
+ showInspector,
12
+ toggleTree,
13
+ } from "../client/store";
14
+
15
+ function showChat() {
16
+ ui.drawer = false;
17
+ ui.inspectorOverlay = false;
18
+ }
19
+ </script>
20
+
21
+ <template>
22
+ <nav class="mnav" aria-label="Panels">
23
+ <button type="button" :class="{ 'is-on': !ui.drawer && !inspectorExpanded }" @click="showChat">
24
+ <MessageSquare :size="18" />Chat
25
+ </button>
26
+ <button
27
+ type="button"
28
+ :class="{ 'is-on': inspectorExpanded }"
29
+ @click="inspectorExpanded ? hideInspector() : showInspector()"
30
+ >
31
+ <PanelRight :size="18" />Details<span v-if="tasks.length" class="badge">{{
32
+ tasks.length
33
+ }}</span>
34
+ </button>
35
+ <button type="button" :class="{ 'is-on': ui.drawer }" @click="toggleTree()">
36
+ <Waypoints :size="18" />Network
37
+ </button>
38
+ </nav>
39
+ </template>