my-durable 0.0.19 → 0.0.21

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.
@@ -7,8 +7,8 @@
7
7
  <meta name="referrer" content="no-referrer" />
8
8
  <link rel="icon" type="image/svg+xml" href="/icons/durable-pi.svg" />
9
9
  <title>Durable Pi | Network</title>
10
- <script type="module" crossorigin src="/assets/index-Cbohux_s.js"></script>
11
- <link rel="stylesheet" crossorigin href="/assets/index-DJZ6A1cN.css">
10
+ <script type="module" crossorigin src="/assets/index-Cv3w8azJ.js"></script>
11
+ <link rel="stylesheet" crossorigin href="/assets/index-BvDyZN2S.css">
12
12
  </head>
13
13
  <body>
14
14
  <div id="app"></div>
@@ -2,7 +2,7 @@
2
2
  // Git chip for the session breadcrumb: branch + dirty at a glance, click for
3
3
  // the real panel — status, worktrees, fetch/pull/log/commit, new worktree.
4
4
  // Everything goes through the device's whitelisted /v1/git API.
5
- import { computed, onBeforeUnmount, onMounted, ref, watch } from "vue";
5
+ import { computed, onBeforeUnmount, onMounted, onServerPrefetch, ref, watch } from "vue";
6
6
  import { ChevronDown, GitBranch, RefreshCw } from "lucide-vue-next";
7
7
  import { deviceId, sessionSummary, notify } from "../client/store";
8
8
  import { deviceClient } from "../client/hub";
@@ -29,9 +29,10 @@ async function refresh() {
29
29
  status.value = null;
30
30
  }
31
31
  }
32
- // First fetch inside setup: SSR (and the first paint) wait for it, so the chip
33
- // never flashes empty; later refreshes ride the interval + cwd changes.
34
- await refresh();
32
+ // Client: fire now, the chip fills reactively. SSR waits on the prefetch
33
+ // hook — a top-level await would need <Suspense> and silently never mount.
34
+ if (!import.meta.env.SSR) void refresh();
35
+ onServerPrefetch(() => refresh());
35
36
  watch(cwd, () => void refresh());
36
37
  onMounted(() => {
37
38
  timer = setInterval(refresh, 15000);
@@ -1,7 +1,7 @@
1
1
  <script setup lang="ts">
2
2
  // Inspector GIT tab: everything the device knows about the session's folder.
3
3
  // Always visible — "not a git repository" is a state, not a reason to hide.
4
- import { computed, onBeforeUnmount, onMounted, ref, watch } from "vue";
4
+ import { computed, onBeforeUnmount, onMounted, onServerPrefetch, ref, watch } from "vue";
5
5
  import { RefreshCw } from "lucide-vue-next";
6
6
  import { deviceId, sessionSummary, notify } from "../client/store";
7
7
  import { deviceClient } from "../client/hub";
@@ -10,6 +10,9 @@ import type { GitStatus } from "@sdk/client";
10
10
  const props = defineProps<{ deviceId?: string; cwd?: string }>();
11
11
  const status = ref<GitStatus | null>(null);
12
12
  const error = ref("");
13
+ const loading = ref(false); // first fetch, nothing to show yet — SAY SO
14
+ const refreshing = ref(false); // refetch with data on screen — dim, keep, never blank
15
+ let inFlight: Promise<void> | undefined;
13
16
  const busy = ref("");
14
17
  const out = ref("");
15
18
  const commitMsg = ref("");
@@ -24,17 +27,35 @@ let timer: ReturnType<typeof setInterval> | undefined;
24
27
  async function refresh() {
25
28
  const c = client();
26
29
  if (!c || !cwd.value) return;
27
- try {
28
- status.value = await c.gitStatus(cwd.value);
29
- error.value = "";
30
- } catch (e) {
31
- status.value = null;
32
- error.value = (e as Error).message.slice(0, 160);
33
- }
30
+ if (inFlight) return inFlight; // interval + session-switch must not pile up
31
+ const first = !status.value;
32
+ if (first) loading.value = true;
33
+ else refreshing.value = true;
34
+ inFlight = (async () => {
35
+ try {
36
+ status.value = await c.gitStatus(cwd.value);
37
+ error.value = "";
38
+ } catch (e) {
39
+ const raw = (e as Error).message ?? "";
40
+ // Keep the LAST GOOD status — a dropped tunnel must not blank the tab
41
+ // the operator is reading (reported: "treo rất lâu", "Cannot reach …").
42
+ error.value = /fetch failed|Cannot reach|Failed to fetch|NetworkError|ERR_NAME/i.test(raw)
43
+ ? `device unreachable — check that it is online, then retry (${raw.slice(0, 80)})`
44
+ : raw.slice(0, 160);
45
+ } finally {
46
+ loading.value = false;
47
+ refreshing.value = false;
48
+ inFlight = undefined;
49
+ }
50
+ })();
51
+ return inFlight;
34
52
  }
35
- // First fetch awaits in setup so SSR and first paint carry the state —
36
- // "not a repository" must be VISIBLE, not a blank tab.
37
- await refresh();
53
+ // NO top-level await: an async setup outside <Suspense> never mounts in the
54
+ // browser ("async setup invoked without a Suspense boundary") — the tab
55
+ // rendered EMPTY (reported: "TAB git trống"). Fire now on the client;
56
+ // onServerPrefetch is what SSR actually waits for.
57
+ if (!import.meta.env.SSR) void refresh();
58
+ onServerPrefetch(() => refresh());
38
59
  onMounted(() => {
39
60
  timer = setInterval(refresh, 10000);
40
61
  });
@@ -120,8 +141,15 @@ async function removeWorktree(path: string) {
120
141
  <button :disabled="!!busy" title="refresh" @click="refresh()"><RefreshCw :size="14" /></button>
121
142
  </header>
122
143
 
123
- <p v-if="error" class="error">{{ error }}</p>
144
+ <p v-if="loading" class="empty">loading git…</p>
145
+ <p v-if="refreshing" class="muted note">refreshing…</p>
146
+
147
+ <p v-if="error" class="error">
148
+ {{ error }}
149
+ <button :disabled="!!busy || !!loading" @click="refresh()">retry</button>
150
+ </p>
124
151
 
152
+ <div v-if="status" :class="{ stale: !!error || refreshing }">
125
153
  <p v-if="status && !status.repo" class="empty">
126
154
  Not a git repository — run <code>git init</code> (or open a session inside a repo) to see branch, status and worktrees here.
127
155
  </p>
@@ -163,6 +191,7 @@ async function removeWorktree(path: string) {
163
191
 
164
192
  <pre v-if="out" class="out">{{ out.slice(0, 6000) }}</pre>
165
193
  </template>
194
+ </div>
166
195
  </template>
167
196
  </div>
168
197
  </template>
@@ -184,5 +213,7 @@ async function removeWorktree(path: string) {
184
213
  .wt-of dd { color: var(--info, #58a6ff); }
185
214
  .out { max-height: 300px; overflow: auto; background: rgb(0 0 0 / 25%); padding: 8px; border-radius: 6px; font-size: 11px; }
186
215
  .empty { opacity: 0.75; }
216
+ .note { font-size: 11px; }
217
+ .stale { opacity: 0.55; transition: opacity 0.2s; }
187
218
  .error { color: var(--danger, #f85149); }
188
219
  </style>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "my-durable",
3
- "version": "0.0.19",
3
+ "version": "0.0.21",
4
4
  "description": "Durable Pi \u2014 the durable agent network: hub web UI, device backend daemon, TUI admin and the shared SDK, packaged as one runnable monorepo install",
5
5
  "license": "MIT",
6
6
  "repository": {