@gmickel/gno 2.5.1 → 2.7.0

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 (160) hide show
  1. package/README.md +60 -5
  2. package/assets/skill/README.md +5 -1
  3. package/assets/skill/SKILL.md +80 -4
  4. package/assets/skill/cli-reference.md +132 -2
  5. package/assets/skill/examples.md +30 -0
  6. package/assets/skill/mcp-reference.md +54 -1
  7. package/assets/skill/recipes/capture-and-file.md +6 -0
  8. package/assets/skill/recipes/memory-file-decision.md +6 -0
  9. package/assets/skill/recipes/memory-supersede-fact.md +5 -0
  10. package/assets/skill/recipes/session-evidence-lookup.md +98 -0
  11. package/assets/spa-production.json.gz +0 -0
  12. package/browser-extension/artifacts/{gno-browser-clipper-v2.5.1.zip → gno-browser-clipper-v2.7.0.zip} +0 -0
  13. package/browser-extension/artifacts/gno-browser-clipper-v2.7.0.zip.sha256 +1 -0
  14. package/browser-extension/dist/manifest.json +1 -1
  15. package/package.json +2 -1
  16. package/spec/cli.md +449 -35
  17. package/spec/mcp.md +234 -4
  18. package/spec/output-schemas/ask.schema.json +1 -1
  19. package/spec/output-schemas/capture-receipt.schema.json +4 -1
  20. package/spec/output-schemas/doctor.schema.json +88 -0
  21. package/spec/output-schemas/error.schema.json +11 -2
  22. package/spec/output-schemas/get.schema.json +1 -1
  23. package/spec/output-schemas/mcp-capture-result.schema.json +4 -2
  24. package/spec/output-schemas/memory-remember.schema.json +10 -4
  25. package/spec/output-schemas/multi-get.schema.json +4 -1
  26. package/spec/output-schemas/peek.schema.json +2 -9
  27. package/spec/output-schemas/request-status.schema.json +113 -0
  28. package/spec/output-schemas/resident-status.schema.json +22 -0
  29. package/spec/output-schemas/search-result.schema.json +1 -1
  30. package/spec/output-schemas/search-results.schema.json +1 -1
  31. package/spec/output-schemas/sessions-automation-run.schema.json +46 -0
  32. package/spec/output-schemas/sessions-discovery.schema.json +38 -0
  33. package/spec/output-schemas/sessions-import-receipt.schema.json +156 -0
  34. package/spec/output-schemas/sessions-status.schema.json +432 -0
  35. package/spec/output-schemas/status.schema.json +98 -0
  36. package/src/cli/commands/ask.ts +14 -2
  37. package/src/cli/commands/capture.ts +55 -96
  38. package/src/cli/commands/daemon.ts +41 -0
  39. package/src/cli/commands/doctor.ts +54 -20
  40. package/src/cli/commands/embed.ts +41 -3
  41. package/src/cli/commands/ls.ts +3 -0
  42. package/src/cli/commands/memory.ts +12 -3
  43. package/src/cli/commands/query.ts +5 -0
  44. package/src/cli/commands/request-status.ts +59 -0
  45. package/src/cli/commands/reset.ts +39 -5
  46. package/src/cli/commands/sessions.ts +713 -0
  47. package/src/cli/commands/shared.ts +14 -1
  48. package/src/cli/commands/status.ts +63 -5
  49. package/src/cli/commands/vec.ts +54 -0
  50. package/src/cli/detach.ts +29 -1
  51. package/src/cli/errors.ts +13 -9
  52. package/src/cli/program.ts +441 -2
  53. package/src/cli/session-binding.ts +49 -0
  54. package/src/config/types.ts +8 -0
  55. package/src/core/capture-publish.ts +239 -0
  56. package/src/core/capture-sync.ts +12 -2
  57. package/src/core/host-paths.ts +31 -0
  58. package/src/core/memory-remember.ts +234 -122
  59. package/src/core/memory-types.ts +11 -0
  60. package/src/core/network-boundary-inventory.ts +8 -0
  61. package/src/core/request-receipts.ts +671 -0
  62. package/src/core/shutdown-budget.ts +6 -0
  63. package/src/core/vector-partition-status.ts +52 -0
  64. package/src/embed/backlog.ts +124 -18
  65. package/src/embed/fingerprint.ts +6 -3
  66. package/src/embed/retry.ts +66 -27
  67. package/src/embed/variant-backlog.ts +15 -10
  68. package/src/embed/variant-retry.ts +31 -22
  69. package/src/index.ts +30 -2
  70. package/src/llm/native-worker/dispatcher.ts +2 -0
  71. package/src/llm/native-worker/embedding-identity.ts +42 -0
  72. package/src/llm/native-worker/protocol.ts +1 -0
  73. package/src/llm/types.ts +3 -0
  74. package/src/mcp/context.ts +17 -0
  75. package/src/mcp/http-egress.ts +4 -0
  76. package/src/mcp/http-transport.ts +2 -0
  77. package/src/mcp/resources/index.ts +6 -5
  78. package/src/mcp/tool-descriptions-core.ts +1 -1
  79. package/src/mcp/tools/capture.ts +87 -85
  80. package/src/mcp/tools/index.ts +77 -4
  81. package/src/mcp/tools/memory-remember.ts +8 -1
  82. package/src/mcp/tools/memory-shared.ts +7 -1
  83. package/src/mcp/tools/request-status.ts +73 -0
  84. package/src/mcp/tools/sessions.ts +208 -0
  85. package/src/mcp/tools/status.ts +4 -0
  86. package/src/pipeline/hybrid.ts +37 -7
  87. package/src/pipeline/vsearch.ts +14 -2
  88. package/src/sdk/client.ts +180 -84
  89. package/src/sdk/index.ts +6 -0
  90. package/src/sdk/types.ts +54 -2
  91. package/src/serve/capture-service.ts +98 -32
  92. package/src/serve/config-sync.ts +3 -2
  93. package/src/serve/embed-scheduler.ts +133 -19
  94. package/src/serve/host-path-redaction.ts +79 -0
  95. package/src/serve/public/app.tsx +4 -1
  96. package/src/serve/public/components/CaptureModal.tsx +26 -8
  97. package/src/serve/public/components/sessions/AutomationPanel.tsx +800 -0
  98. package/src/serve/public/components/sessions/ImportReceipt.tsx +238 -0
  99. package/src/serve/public/components/sessions/SessionSearch.tsx +286 -0
  100. package/src/serve/public/components/sessions/SourcesPanel.tsx +541 -0
  101. package/src/serve/public/components/sessions/api.ts +40 -0
  102. package/src/serve/public/globals.built.css +1 -1
  103. package/src/serve/public/hooks/use-api.ts +26 -3
  104. package/src/serve/public/lib/request-intent.ts +77 -0
  105. package/src/serve/public/lib/snippet.tsx +52 -0
  106. package/src/serve/public/lib/workspace-actions.ts +12 -1
  107. package/src/serve/public/lib/workspace-tabs.ts +2 -0
  108. package/src/serve/public/pages/Dashboard.tsx +22 -9
  109. package/src/serve/public/pages/DocView.tsx +25 -6
  110. package/src/serve/public/pages/DocumentEditor.tsx +224 -104
  111. package/src/serve/public/pages/Search.tsx +1 -41
  112. package/src/serve/public/pages/Sessions.tsx +350 -0
  113. package/src/serve/resident-runtime.ts +69 -4
  114. package/src/serve/resident-status.ts +13 -1
  115. package/src/serve/routes/api.ts +476 -147
  116. package/src/serve/routes/sessions.ts +766 -0
  117. package/src/serve/security.ts +9 -0
  118. package/src/serve/server.ts +215 -10
  119. package/src/serve/session-automation.ts +146 -0
  120. package/src/serve/status-model.ts +16 -0
  121. package/src/serve/status.ts +2 -0
  122. package/src/serve/watch-reconciliation-shared.ts +3 -0
  123. package/src/serve/watch-service-events.ts +3 -2
  124. package/src/serve/watch-service-run-flush.ts +35 -2
  125. package/src/serve/watch-service.ts +5 -0
  126. package/src/sessions/archive.ts +348 -0
  127. package/src/sessions/automation-state.ts +444 -0
  128. package/src/sessions/automation-status.ts +239 -0
  129. package/src/sessions/automation.ts +1169 -0
  130. package/src/sessions/binding.ts +105 -0
  131. package/src/sessions/claude-hook.ts +240 -0
  132. package/src/sessions/config.ts +176 -0
  133. package/src/sessions/format.ts +191 -0
  134. package/src/sessions/import-child-env.ts +8 -0
  135. package/src/sessions/import-child.ts +152 -0
  136. package/src/sessions/parsers/claude-code.ts +259 -0
  137. package/src/sessions/parsers/codex.ts +303 -0
  138. package/src/sessions/parsers/hermes.ts +248 -0
  139. package/src/sessions/parsers/openclaw.ts +496 -0
  140. package/src/sessions/parsers/shared.ts +184 -0
  141. package/src/sessions/sanitize.ts +222 -0
  142. package/src/sessions/service.ts +1533 -0
  143. package/src/sessions/setup.ts +477 -0
  144. package/src/sessions/sources.ts +518 -0
  145. package/src/sessions/state.ts +118 -0
  146. package/src/sessions/types.ts +457 -0
  147. package/src/store/migrations/031-runtime-independent-vectors.ts +29 -0
  148. package/src/store/migrations/032-vector-runtime-callers.ts +17 -0
  149. package/src/store/migrations/index.ts +4 -0
  150. package/src/store/sqlite/adapter.ts +76 -16
  151. package/src/store/sqlite/scoped-index.ts +9 -0
  152. package/src/store/types.ts +11 -1
  153. package/src/store/vector/lazy.ts +46 -43
  154. package/src/store/vector/runtime-compat.ts +651 -0
  155. package/src/store/vector/sqlite-vec.ts +20 -2
  156. package/src/store/vector/status.ts +276 -35
  157. package/src/store/vector/types.ts +2 -0
  158. package/src/store/vector/variant-search.ts +71 -23
  159. package/src/store/vector/variants.ts +49 -14
  160. package/browser-extension/artifacts/gno-browser-clipper-v2.5.1.zip.sha256 +0 -1
@@ -1,5 +1,7 @@
1
1
  import { useCallback, useState } from "react";
2
2
 
3
+ import { writeOutcomeUnknown } from "../lib/request-intent";
4
+
3
5
  interface ApiState<T> {
4
6
  data: T | null;
5
7
  loading: boolean;
@@ -81,7 +83,17 @@ export function useApi<T>() {
81
83
  export async function apiFetch<T>(
82
84
  endpoint: string,
83
85
  options?: RequestInit
84
- ): Promise<{ data: T | null; error: string | null }> {
86
+ ): Promise<{
87
+ data: T | null;
88
+ error: string | null;
89
+ /** `error.details` of the JSON error envelope, when the server sent one. */
90
+ details?: Record<string, unknown>;
91
+ /**
92
+ * A write may or may not have committed: no readable response, a server
93
+ * error, or the request is still pending under its request ID.
94
+ */
95
+ outcomeUnknown?: true;
96
+ }> {
85
97
  try {
86
98
  const res = await fetch(endpoint, {
87
99
  headers: { "Content-Type": "application/json" },
@@ -91,14 +103,24 @@ export async function apiFetch<T>(
91
103
  const { json, parseError } = await parseJsonSafe(res);
92
104
 
93
105
  if (parseError) {
94
- return { data: null, error: parseError };
106
+ return { data: null, error: parseError, outcomeUnknown: true };
95
107
  }
96
108
 
97
109
  if (!res.ok) {
98
- const apiError = json as { error?: { message?: string } };
110
+ const apiError = json as {
111
+ error?: {
112
+ code?: string;
113
+ message?: string;
114
+ details?: Record<string, unknown>;
115
+ };
116
+ };
99
117
  return {
100
118
  data: null,
101
119
  error: apiError.error?.message || `Request failed: ${res.status}`,
120
+ details: apiError.error?.details,
121
+ ...(writeOutcomeUnknown(res.status, apiError.error?.code)
122
+ ? { outcomeUnknown: true as const }
123
+ : {}),
102
124
  };
103
125
  }
104
126
 
@@ -107,6 +129,7 @@ export async function apiFetch<T>(
107
129
  return {
108
130
  data: null,
109
131
  error: err instanceof Error ? err.message : "Network error",
132
+ outcomeUnknown: true,
110
133
  };
111
134
  }
112
135
  }
@@ -0,0 +1,77 @@
1
+ /**
2
+ * One request ID per submitted write intent.
3
+ *
4
+ * Retrying the same payload (after a lost response, an error, or a page
5
+ * refresh) reuses the ID, so the server replays the committed outcome instead
6
+ * of writing twice. A changed payload is a new intent and gets a new ID. The
7
+ * unresolved intent is kept in sessionStorage so it survives a refresh of
8
+ * this tab; it is cleared once the write is confirmed.
9
+ */
10
+ export interface RequestIntent {
11
+ key: string;
12
+ requestId: string;
13
+ }
14
+
15
+ /** Compact payload fingerprint (cyrb53); a collision only yields a conflict error. */
16
+ function fingerprint(text: string): string {
17
+ let h1 = 0xde_ad_be_ef;
18
+ let h2 = 0x41_c6_ce_57;
19
+ for (let index = 0; index < text.length; index += 1) {
20
+ const code = text.charCodeAt(index);
21
+ h1 = Math.imul(h1 ^ code, 2_654_435_761);
22
+ h2 = Math.imul(h2 ^ code, 1_597_334_677);
23
+ }
24
+ h1 = Math.imul(h1 ^ (h1 >>> 16), 2_246_822_507);
25
+ h1 ^= Math.imul(h2 ^ (h2 >>> 13), 3_266_489_909);
26
+ h2 = Math.imul(h2 ^ (h2 >>> 16), 2_246_822_507);
27
+ h2 ^= Math.imul(h1 ^ (h1 >>> 13), 3_266_489_909);
28
+ return (4_294_967_296 * (2_097_151 & h2) + (h1 >>> 0)).toString(36);
29
+ }
30
+
31
+ function readStored(storageKey: string): RequestIntent | null {
32
+ try {
33
+ const raw = sessionStorage.getItem(storageKey);
34
+ return raw ? (JSON.parse(raw) as RequestIntent) : null;
35
+ } catch {
36
+ return null;
37
+ }
38
+ }
39
+
40
+ export function requestIdForIntent(
41
+ slot: { current: RequestIntent | null },
42
+ payload: string,
43
+ storageKey: string
44
+ ): string {
45
+ const key = fingerprint(payload);
46
+ slot.current ??= readStored(storageKey);
47
+ if (slot.current?.key !== key) {
48
+ slot.current = { key, requestId: crypto.randomUUID() };
49
+ try {
50
+ sessionStorage.setItem(storageKey, JSON.stringify(slot.current));
51
+ } catch {
52
+ // Storage unavailable: the ID still covers retries in this page.
53
+ }
54
+ }
55
+ return slot.current.requestId;
56
+ }
57
+
58
+ /** The intent's write is confirmed: the next submit is a new intent. */
59
+ export function clearRequestIntent(
60
+ slot: { current: RequestIntent | null },
61
+ storageKey: string
62
+ ): void {
63
+ slot.current = null;
64
+ try {
65
+ sessionStorage.removeItem(storageKey);
66
+ } catch {
67
+ // Nothing persisted to clear.
68
+ }
69
+ }
70
+
71
+ /**
72
+ * Whether a failed write's HTTP answer leaves its commit unknown: a server
73
+ * error, or the request ID's earlier attempt is still in progress.
74
+ */
75
+ export function writeOutcomeUnknown(status: number, code?: string): boolean {
76
+ return status >= 500 || code === "REQUEST_PENDING";
77
+ }
@@ -0,0 +1,52 @@
1
+ import type { ReactNode } from "react";
2
+
3
+ const MARK_OPEN = "<mark>";
4
+ const MARK_CLOSE = "</mark>";
5
+
6
+ /** CommonMark backslash escape: `\` before any ASCII punctuation character. */
7
+ const MARKDOWN_ESCAPE = /\\([!"#$%&'()*+,\-./:;<=>?@[\\\]^_`{|}~])/g;
8
+
9
+ /** Drop Markdown backslash escapes (`state.db\#` -> `state.db#`). */
10
+ export function unescapeMarkdown(text: string): string {
11
+ return text.replace(MARKDOWN_ESCAPE, "$1");
12
+ }
13
+
14
+ /**
15
+ * Render a search snippet: FTS `<mark>` highlights become real <mark>
16
+ * elements, everything else stays React text (never parsed as HTML).
17
+ * Markdown backslash escapes are dropped because indexed text keeps them.
18
+ */
19
+ export function renderSnippet(snippet: string): ReactNode[] {
20
+ const parts: ReactNode[] = [];
21
+ let remaining = snippet;
22
+ let key = 0;
23
+
24
+ while (remaining.length > 0) {
25
+ const markStart = remaining.indexOf(MARK_OPEN);
26
+ if (markStart === -1) {
27
+ parts.push(unescapeMarkdown(remaining));
28
+ break;
29
+ }
30
+ if (markStart > 0) {
31
+ parts.push(unescapeMarkdown(remaining.slice(0, markStart)));
32
+ }
33
+ const markEnd = remaining.indexOf(MARK_CLOSE, markStart);
34
+ if (markEnd === -1) {
35
+ parts.push(unescapeMarkdown(remaining.slice(markStart)));
36
+ break;
37
+ }
38
+ parts.push(
39
+ <mark
40
+ className="rounded bg-primary/20 px-0.5 font-medium text-primary"
41
+ key={key++}
42
+ >
43
+ {unescapeMarkdown(
44
+ remaining.slice(markStart + MARK_OPEN.length, markEnd)
45
+ )}
46
+ </mark>
47
+ );
48
+ remaining = remaining.slice(markEnd + MARK_CLOSE.length);
49
+ }
50
+
51
+ return parts;
52
+ }
@@ -14,7 +14,8 @@ export type WorkspaceActionId =
14
14
  | "go-ask"
15
15
  | "go-graph"
16
16
  | "go-collections"
17
- | "go-connectors";
17
+ | "go-connectors"
18
+ | "go-sessions";
18
19
 
19
20
  export interface WorkspaceAction {
20
21
  id: WorkspaceActionId;
@@ -155,6 +156,13 @@ export function getWorkspaceActions(
155
156
  keywords: ["connectors", "mcp", "skills", "agents"],
156
157
  available: true,
157
158
  },
159
+ {
160
+ id: "go-sessions",
161
+ group: "Go To",
162
+ label: "Agent sessions",
163
+ keywords: ["sessions", "archive", "transcripts", "import", "agents"],
164
+ available: true,
165
+ },
158
166
  ];
159
167
  }
160
168
 
@@ -220,6 +228,9 @@ export function runWorkspaceAction(
220
228
  case "go-connectors":
221
229
  handlers.navigate("/connectors");
222
230
  break;
231
+ case "go-sessions":
232
+ handlers.navigate("/sessions");
233
+ break;
223
234
  }
224
235
 
225
236
  handlers.closePalette();
@@ -69,6 +69,8 @@ function getLocationLabel(location: string): string {
69
69
  return "Compiled context";
70
70
  case "/traces":
71
71
  return "Trace history";
72
+ case "/sessions":
73
+ return "Agent sessions";
72
74
  case "/doc":
73
75
  case "/edit": {
74
76
  const uri = params.get("uri") ?? "";
@@ -1,4 +1,5 @@
1
1
  import {
2
+ ArchiveIcon,
2
3
  BookOpen,
3
4
  CheckCircle2Icon,
4
5
  CpuIcon,
@@ -540,6 +541,15 @@ export default function Dashboard({ navigate }: PageProps) {
540
541
  <HistoryIcon className="size-4" />
541
542
  Trace history
542
543
  </Button>
544
+ <Button
545
+ className="gap-2"
546
+ onClick={() => navigate("/sessions")}
547
+ size="lg"
548
+ variant="outline"
549
+ >
550
+ <ArchiveIcon className="size-4" />
551
+ Agent sessions
552
+ </Button>
543
553
  <Button
544
554
  onClick={() => navigate("/context/compiled")}
545
555
  size="lg"
@@ -778,25 +788,28 @@ export default function Dashboard({ navigate }: PageProps) {
778
788
  }
779
789
  style={{ animationDelay: `${0.4 + index * 0.1}s` }}
780
790
  >
781
- <CardContent className="flex items-center justify-between py-4">
782
- <div className="flex items-center gap-3">
791
+ <CardContent className="flex flex-wrap items-center justify-between gap-3 py-4">
792
+ <div className="flex min-w-0 flex-1 basis-48 items-center gap-3">
783
793
  {syncing ? (
784
- <Loader2Icon className="size-4 animate-spin text-amber-500" />
794
+ <Loader2Icon className="size-4 shrink-0 animate-spin text-amber-500" />
785
795
  ) : collection.embeddedCount >= collection.chunkCount ? (
786
- <CheckCircle2Icon className="size-4 text-green-500" />
796
+ <CheckCircle2Icon className="size-4 shrink-0 text-green-500" />
787
797
  ) : (
788
- <div className="size-4 rounded-full border-2 border-amber-500" />
798
+ <div className="size-4 shrink-0 rounded-full border-2 border-amber-500" />
789
799
  )}
790
- <div>
791
- <div className="font-medium text-lg transition-colors group-hover:text-primary">
800
+ <div className="min-w-0">
801
+ <div className="truncate font-medium text-lg transition-colors group-hover:text-primary">
792
802
  {collection.name}
793
803
  </div>
794
- <div className="font-mono text-muted-foreground text-sm">
804
+ <div
805
+ className="truncate font-mono text-muted-foreground text-sm"
806
+ title={collection.path}
807
+ >
795
808
  {collection.path}
796
809
  </div>
797
810
  </div>
798
811
  </div>
799
- <div className="flex items-center gap-3 text-right">
812
+ <div className="flex shrink-0 items-center gap-3 text-right">
800
813
  <Button
801
814
  onClick={(event) => {
802
815
  event.stopPropagation();
@@ -37,6 +37,7 @@ import {
37
37
  type FileRefactorPreviewPlan,
38
38
  } from "../../../core/file-refactor-contract";
39
39
  import { extractSections } from "../../../core/sections";
40
+ import { updateFrontmatterTags } from "../../../ingestion/frontmatter";
40
41
  import {
41
42
  CodeBlock,
42
43
  CodeBlockCopyButton,
@@ -83,6 +84,13 @@ import {
83
84
  isPdfDocument,
84
85
  } from "../lib/doc-asset-url";
85
86
  import { waitForDocumentAvailability } from "../lib/document-availability";
87
+ import {
88
+ clearRequestIntent,
89
+ type RequestIntent,
90
+ requestIdForIntent,
91
+ } from "../lib/request-intent";
92
+
93
+ const TAG_INTENT_KEY = "gno:tag-intent";
86
94
  import {
87
95
  buildReadableSectionUrl,
88
96
  createCitationSectionUrl,
@@ -576,11 +584,10 @@ export default function DocView({ navigate }: PageProps) {
576
584
  return buildDocAssetUrl(doc.uri, doc.relPath);
577
585
  }, [doc, isPdf]);
578
586
 
579
- // Remote "Open original" target for any document that has a source file:
580
- // /api/doc-asset serves any collection file inline, so this keeps the
581
- // previous file:// scope (every read-only source) for remote clients.
587
+ // Remote "Open original" target: /api/doc-asset serves any collection file
588
+ // inline by URI, and remote clients never receive the host absPath.
582
589
  const sourceAssetUrl = useMemo(() => {
583
- if (!doc?.source.absPath) {
590
+ if (!doc) {
584
591
  return null;
585
592
  }
586
593
  return buildDocAssetUrl(doc.uri, doc.relPath);
@@ -1201,7 +1208,8 @@ export default function DocView({ navigate }: PageProps) {
1201
1208
  setTagSaveError(null);
1202
1209
  }, []);
1203
1210
 
1204
- // Save tags
1211
+ // Save tags (one request ID per tag-set intent, reused on retry)
1212
+ const tagIntentRef = useRef<RequestIntent | null>(null);
1205
1213
  const handleSaveTags = useCallback(async () => {
1206
1214
  if (!doc) return;
1207
1215
 
@@ -1218,6 +1226,11 @@ export default function DocView({ navigate }: PageProps) {
1218
1226
  expectedSourceHash: doc.source.sourceHash,
1219
1227
  expectedModifiedAt: doc.source.modifiedAt,
1220
1228
  uri: doc.uri,
1229
+ requestId: requestIdForIntent(
1230
+ tagIntentRef,
1231
+ `${doc.uri}\u0000${doc.source.sourceHash}\u0000${editedTags.join(",")}`,
1232
+ TAG_INTENT_KEY
1233
+ ),
1221
1234
  }),
1222
1235
  }
1223
1236
  );
@@ -1229,10 +1242,16 @@ export default function DocView({ navigate }: PageProps) {
1229
1242
  return;
1230
1243
  }
1231
1244
 
1232
- // Update doc with new tags
1245
+ clearRequestIntent(tagIntentRef, TAG_INTENT_KEY);
1246
+ // Mirror the committed write-back so the frontmatter tag list is current
1247
+ // without waiting for the index to resync.
1233
1248
  setDoc({
1234
1249
  ...doc,
1235
1250
  tags: editedTags,
1251
+ content:
1252
+ data?.writeBack === "applied" && doc.content !== null
1253
+ ? updateFrontmatterTags(doc.content, editedTags)
1254
+ : doc.content,
1236
1255
  source: {
1237
1256
  ...doc.source,
1238
1257
  sourceHash: data?.version.sourceHash ?? doc.source.sourceHash,