@hienlh/ppm 0.13.20 → 0.13.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.
Files changed (86) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/assets/skills/ppm/SKILL.md +1 -1
  3. package/assets/skills/ppm/references/http-api.md +1 -1
  4. package/dist/web/assets/{ai-settings-section-DR5BueEL.js → ai-settings-section-DN4egS8e.js} +1 -1
  5. package/dist/web/assets/architecture-PBZL5I3N-CZBayZMd.js +1 -0
  6. package/dist/web/assets/{audio-preview-DwyrUe-V.js → audio-preview-Bit1BkEv.js} +1 -1
  7. package/dist/web/assets/chat-tab-LuR2CwiB.js +12 -0
  8. package/dist/web/assets/code-editor-DES3rcVN.js +8 -0
  9. package/dist/web/assets/{conflict-editor-C8vTvS9w.js → conflict-editor-upKOD9uO.js} +1 -1
  10. package/dist/web/assets/{csv-preview-Bo-N3GHl.js → csv-preview-BIfojSWd.js} +1 -1
  11. package/dist/web/assets/{data-grid-overlay-editor-DqcDQ9st.js → data-grid-overlay-editor-DZIqEOsz.js} +1 -1
  12. package/dist/web/assets/{database-viewer-_RTlPC26.js → database-viewer-N6OCfZs9.js} +1 -1
  13. package/dist/web/assets/{diff-viewer-P2Dc__bQ.js → diff-viewer-B1JmhayU.js} +1 -1
  14. package/dist/web/assets/{esm-Dvc8oJly.js → esm-UZtw2QcY.js} +1 -1
  15. package/dist/web/assets/{extension-webview-CHqtkQBd.js → extension-webview-BHHiMswb.js} +2 -2
  16. package/dist/web/assets/gitGraph-HDMCJU4V-CboO1wK8.js +1 -0
  17. package/dist/web/assets/{glide-data-grid-9TPVejSQ.js → glide-data-grid-DBN29kPX.js} +6 -6
  18. package/dist/web/assets/{image-preview--nh-wHgF.js → image-preview-XYXkVEGO.js} +1 -1
  19. package/dist/web/assets/index-C5sLGvFC.css +2 -0
  20. package/dist/web/assets/{index-xpTdWKsA.js → index-EaYSB9U9.js} +13 -13
  21. package/dist/web/assets/info-3K5VOQVL-D_qKNgUf.js +1 -0
  22. package/dist/web/assets/keybindings-store-fGywATlN.js +1 -0
  23. package/dist/web/assets/{markdown-renderer-Bsow9WVr.js → markdown-renderer-DSFZBOpD.js} +3 -3
  24. package/dist/web/assets/notification-store-Dz9dmEg3.js +1 -0
  25. package/dist/web/assets/{number-overlay-editor-XTjjEXtk.js → number-overlay-editor-CewUR5pB.js} +1 -1
  26. package/dist/web/assets/packet-RMMSAZCW-XtGc2GdX.js +1 -0
  27. package/dist/web/assets/{pdf-preview-BqntOcNA.js → pdf-preview-Bz2JkLQ6.js} +1 -1
  28. package/dist/web/assets/pie-UPGHQEXC-DNZ5YtCW.js +1 -0
  29. package/dist/web/assets/{port-forwarding-tab-WWRLWcTB.js → port-forwarding-tab-s0cGnGgx.js} +1 -1
  30. package/dist/web/assets/{postgres-viewer-g7-3kOzD.js → postgres-viewer-DwELE9sG.js} +3 -3
  31. package/dist/web/assets/radar-KQ55EAFF-uCGpAvZE.js +1 -0
  32. package/dist/web/assets/{settings-store-D2MtC9tm.js → settings-store-CVrIYYCB.js} +2 -2
  33. package/dist/web/assets/settings-tab-D6zXU5c_.js +1 -0
  34. package/dist/web/assets/{sql-query-editor-CultKZsI.js → sql-query-editor-CMPsQprT.js} +1 -1
  35. package/dist/web/assets/sqlite-viewer-BL0Z_xor.js +1 -0
  36. package/dist/web/assets/terminal-tab-CqSN73E-.js +1 -0
  37. package/dist/web/assets/treemap-KZPCXAKY-DQvivjBa.js +1 -0
  38. package/dist/web/assets/{use-monaco-theme-CugUkORI.js → use-monaco-theme-BePWbY58.js} +1 -1
  39. package/dist/web/assets/{vendor-mermaid-CPtQ2zua.js → vendor-mermaid-Cl50p6TB.js} +2 -2
  40. package/dist/web/assets/{video-preview-C4PxtiOc.js → video-preview-Y5NIrm_u.js} +1 -1
  41. package/dist/web/index.html +6 -6
  42. package/dist/web/sw.js +1 -1
  43. package/package.json +1 -1
  44. package/src/providers/claude-agent-sdk.ts +13 -1
  45. package/src/server/routes/chat.ts +73 -3
  46. package/src/server/routes/database.ts +11 -2
  47. package/src/server/ws/chat.ts +12 -3
  48. package/src/services/autostart-generator.ts +2 -2
  49. package/src/services/autostart-register.ts +6 -3
  50. package/src/services/db.service.ts +41 -1
  51. package/src/services/supervisor.ts +20 -7
  52. package/src/web/app.tsx +8 -0
  53. package/src/web/components/chat/chat-history-bar.tsx +43 -13
  54. package/src/web/components/chat/chat-tab.tsx +3 -0
  55. package/src/web/components/chat/session-list-panel.tsx +15 -8
  56. package/src/web/components/chat/session-picker.tsx +33 -5
  57. package/src/web/components/database/connection-list.tsx +55 -205
  58. package/src/web/components/database/connection-row.tsx +104 -0
  59. package/src/web/components/database/database-sidebar.tsx +1 -1
  60. package/src/web/components/database/schema-table-tree.tsx +98 -0
  61. package/src/web/components/database/use-connections.ts +9 -6
  62. package/src/web/hooks/use-chat.ts +9 -2
  63. package/src/web/hooks/use-debounced-value.ts +10 -0
  64. package/src/web/stores/notification-store.ts +42 -0
  65. package/dist/web/assets/architecture-PBZL5I3N-7JKY4P1L.js +0 -1
  66. package/dist/web/assets/chat-tab-DqS9Qk3O.js +0 -12
  67. package/dist/web/assets/code-editor-DwdeigGe.js +0 -8
  68. package/dist/web/assets/gitGraph-HDMCJU4V-Daf9rhiF.js +0 -1
  69. package/dist/web/assets/index-nC9UURj4.css +0 -2
  70. package/dist/web/assets/info-3K5VOQVL-gn0pjNiT.js +0 -1
  71. package/dist/web/assets/keybindings-store-C0XkvJcm.js +0 -1
  72. package/dist/web/assets/packet-RMMSAZCW-Csaeizjc.js +0 -1
  73. package/dist/web/assets/pie-UPGHQEXC-DatkjxTH.js +0 -1
  74. package/dist/web/assets/radar-KQ55EAFF-BnGB20hR.js +0 -1
  75. package/dist/web/assets/settings-tab-DO3s244B.js +0 -1
  76. package/dist/web/assets/sqlite-viewer-BtYh66b0.js +0 -1
  77. package/dist/web/assets/terminal-tab-C25rc_34.js +0 -1
  78. package/dist/web/assets/treemap-KZPCXAKY-CgEYv38e.js +0 -1
  79. /package/dist/web/assets/{api-settings-DowGyuVy.js → api-settings-DnHv6JgF.js} +0 -0
  80. /package/dist/web/assets/{data-grid-types-DqqspyVw.js → data-grid-types-BISkUXAY.js} +0 -0
  81. /package/dist/web/assets/{dist-_jZs3YZC.js → dist-B1I_4Jtc.js} +0 -0
  82. /package/dist/web/assets/{dist-D1SZxtVS.js → dist-CcDNqGjt.js} +0 -0
  83. /package/dist/web/assets/{katex-DzXRfQ_m.js → katex-Bqvo_ZG0.js} +0 -0
  84. /package/dist/web/assets/{lib-Dub8DlCJ.js → lib-Bu71-TFS.js} +0 -0
  85. /package/dist/web/assets/{use-blob-url-DGY5qKiT.js → use-blob-url-QX-XajU8.js} +0 -0
  86. /package/dist/web/assets/{vendor-xterm-Dyfw49hJ.js → vendor-xterm-K3_Xwigj.js} +0 -0
@@ -0,0 +1,104 @@
1
+ import { ChevronRight, ChevronDown, RefreshCw, Pencil, Trash2, Lock, Search } from "lucide-react";
2
+ import { cn } from "@/lib/utils";
3
+ import type { Connection, CachedTable } from "./use-connections";
4
+ import { SchemaTableTree, type ColumnInfo } from "./schema-table-tree";
5
+
6
+ interface ConnectionRowProps {
7
+ conn: Connection;
8
+ isExpanded: boolean;
9
+ isRefreshing: boolean;
10
+ tables: CachedTable[];
11
+ schemas: Map<string, CachedTable[]>;
12
+ isSingleSchema: boolean;
13
+ filter: string;
14
+ expandedTables: Set<string>;
15
+ loadingColumns: Set<string>;
16
+ columnCache?: Map<string, ColumnInfo[]>;
17
+ columnErrors?: Set<string>;
18
+ refreshError?: string;
19
+ hasGroup: boolean;
20
+ onToggle: (id: number, autoRefresh: boolean) => void;
21
+ onRefresh: (id: number) => void;
22
+ onEdit: (conn: Connection) => void;
23
+ onDelete: (id: number) => void;
24
+ onOpenTable: (conn: Connection, tableName: string, schemaName: string) => void;
25
+ onToggleTable: (connId: number, tableName: string, schemaName: string) => void;
26
+ onFilterChange: (connId: number, value: string) => void;
27
+ }
28
+
29
+ export function ConnectionRow({
30
+ conn, isExpanded, isRefreshing, tables, schemas, isSingleSchema,
31
+ filter, expandedTables, loadingColumns, columnCache, columnErrors,
32
+ refreshError, hasGroup,
33
+ onToggle, onRefresh, onEdit, onDelete, onOpenTable, onToggleTable, onFilterChange,
34
+ }: ConnectionRowProps) {
35
+ const handleToggle = () => onToggle(conn.id, !isExpanded && tables.length === 0);
36
+
37
+ return (
38
+ <div>
39
+ <div className={cn("group flex items-center gap-1 py-1 hover:bg-surface-elevated transition-colors", hasGroup ? "pl-3 pr-2" : "px-2")}>
40
+ <button onClick={handleToggle} className="shrink-0 text-text-subtle hover:text-foreground transition-colors">
41
+ {isExpanded ? <ChevronDown className="size-3" /> : <ChevronRight className="size-3" />}
42
+ </button>
43
+ <span className="shrink-0 size-2 rounded-full border border-border" style={{ backgroundColor: conn.color ?? "transparent" }} />
44
+ <button className="flex-1 text-left text-xs truncate hover:text-primary transition-colors" onClick={handleToggle}>
45
+ {conn.name}
46
+ </button>
47
+ <span className="shrink-0 text-[9px] text-text-subtle uppercase px-1 rounded bg-surface-elevated">
48
+ {conn.type === "postgres" ? "PG" : "DB"}
49
+ </span>
50
+ {conn.readonly === 1 && <span title="Readonly"><Lock className="shrink-0 size-2.5 text-text-subtle" /></span>}
51
+ <div className="flex can-hover:hidden can-hover:group-hover:flex items-center gap-0.5 shrink-0">
52
+ <button onClick={() => onRefresh(conn.id)} disabled={isRefreshing} className="p-0.5 text-text-subtle hover:text-foreground transition-colors" title="Refresh tables">
53
+ <RefreshCw className={cn("size-3", isRefreshing && "animate-spin")} />
54
+ </button>
55
+ <button onClick={() => onEdit(conn)} className="p-0.5 text-text-subtle hover:text-foreground transition-colors" title="Edit">
56
+ <Pencil className="size-3" />
57
+ </button>
58
+ <button onClick={() => onDelete(conn.id)} className="p-0.5 text-text-subtle hover:text-red-500 transition-colors" title="Delete">
59
+ <Trash2 className="size-3" />
60
+ </button>
61
+ </div>
62
+ </div>
63
+
64
+ {isExpanded && (
65
+ <div className="ml-[11px] border-l border-dashed border-border pl-1">
66
+ {isRefreshing && tables.length === 0 && <p className="text-[10px] text-text-subtle px-2 py-1">Loading…</p>}
67
+ {!isRefreshing && tables.length === 0 && (
68
+ refreshError
69
+ ? <p className="text-[10px] text-red-500 px-2 py-1 break-all">{refreshError}</p>
70
+ : <p className="text-[10px] text-text-subtle px-2 py-1">No tables cached</p>
71
+ )}
72
+ {tables.length > 0 && (
73
+ <>
74
+ {tables.length > 5 && (
75
+ <div className="flex items-center gap-1 px-2 py-0.5">
76
+ <Search className="size-2.5 text-text-subtle shrink-0" />
77
+ <input
78
+ type="text"
79
+ value={filter}
80
+ onChange={(e) => onFilterChange(conn.id, e.target.value)}
81
+ placeholder="Filter tables…"
82
+ className="w-full text-[10px] bg-transparent border-none outline-none text-foreground placeholder:text-text-subtle"
83
+ />
84
+ </div>
85
+ )}
86
+ <SchemaTableTree
87
+ connId={conn.id}
88
+ schemas={schemas}
89
+ isSingleSchema={isSingleSchema}
90
+ filter={filter}
91
+ expandedTables={expandedTables}
92
+ loadingColumns={loadingColumns}
93
+ columnCache={columnCache}
94
+ columnErrors={columnErrors}
95
+ onToggleTable={onToggleTable}
96
+ onOpenTable={(tableName, schemaName) => onOpenTable(conn, tableName, schemaName)}
97
+ />
98
+ </>
99
+ )}
100
+ </div>
101
+ )}
102
+ </div>
103
+ );
104
+ }
@@ -24,7 +24,7 @@ export function DatabaseSidebar() {
24
24
 
25
25
  const handleDelete = async (id: number) => {
26
26
  if (!confirm("Delete this connection?")) return;
27
- await deleteConnection(id);
27
+ try { await deleteConnection(id); } catch { /* server error — connection list will stay in sync on next fetch */ }
28
28
  };
29
29
 
30
30
  const handleCreate = async (data: CreateConnectionData) => {
@@ -0,0 +1,98 @@
1
+ import { ChevronRight, ChevronDown, Database, Key, Link2 } from "lucide-react";
2
+ import type { CachedTable } from "./use-connections";
3
+
4
+ export interface ColumnInfo {
5
+ name: string;
6
+ type: string;
7
+ nullable: boolean;
8
+ pk: boolean;
9
+ fk: { table: string; column: string } | null;
10
+ }
11
+
12
+ interface SchemaTableTreeProps {
13
+ connId: number;
14
+ schemas: Map<string, CachedTable[]>;
15
+ isSingleSchema: boolean;
16
+ filter: string;
17
+ expandedTables: Set<string>;
18
+ loadingColumns: Set<string>;
19
+ columnCache?: Map<string, ColumnInfo[]>;
20
+ columnErrors?: Set<string>;
21
+ onToggleTable: (connId: number, tableName: string, schemaName: string) => void;
22
+ onOpenTable: (tableName: string, schemaName: string) => void;
23
+ }
24
+
25
+ export function SchemaTableTree({
26
+ connId, schemas, isSingleSchema, filter,
27
+ expandedTables, loadingColumns, columnCache, columnErrors,
28
+ onToggleTable, onOpenTable,
29
+ }: SchemaTableTreeProps) {
30
+ const filterLower = filter.toLowerCase();
31
+
32
+ return (
33
+ <div className="overflow-y-auto max-h-[40vh]">
34
+ {Array.from(schemas.entries()).map(([schemaName, tables]) => {
35
+ const filteredTables = filterLower
36
+ ? tables.filter((t) => t.tableName.toLowerCase().includes(filterLower))
37
+ : tables;
38
+ if (filteredTables.length === 0) return null;
39
+
40
+ return (
41
+ <div key={schemaName}>
42
+ {!isSingleSchema && (
43
+ <p className="px-2 py-0.5 text-[9px] font-semibold text-text-subtle uppercase tracking-wider">{schemaName}</p>
44
+ )}
45
+ {filteredTables.map((t) => {
46
+ const tableKey = `${connId}:${t.schemaName}.${t.tableName}`;
47
+ const isTableExpanded = expandedTables.has(tableKey);
48
+ const isLoadingCols = loadingColumns.has(tableKey);
49
+ const columns = columnCache?.get(tableKey);
50
+ const hasError = columnErrors?.has(tableKey);
51
+
52
+ return (
53
+ <div key={tableKey}>
54
+ <div className="flex items-center gap-1 pl-2 pr-2 py-0.5 hover:bg-surface-elevated transition-colors group/table">
55
+ <button
56
+ onClick={() => onToggleTable(connId, t.tableName, t.schemaName)}
57
+ className="shrink-0 text-text-subtle hover:text-foreground transition-colors"
58
+ >
59
+ {isTableExpanded ? <ChevronDown className="size-2.5" /> : <ChevronRight className="size-2.5" />}
60
+ </button>
61
+ <Database className="size-2.5 shrink-0 text-text-subtle" />
62
+ <button
63
+ onClick={() => onOpenTable(t.tableName, t.schemaName)}
64
+ className="flex-1 text-left text-[11px] text-text-secondary hover:text-foreground transition-colors truncate"
65
+ >
66
+ {t.tableName}
67
+ </button>
68
+ <span className="text-[9px] text-text-subtle">{t.rowCount}</span>
69
+ </div>
70
+
71
+ {isTableExpanded && (
72
+ <div className="ml-[18px] border-l border-dotted border-border pl-2">
73
+ {isLoadingCols && <p className="text-[9px] text-text-subtle px-1 py-0.5">Loading…</p>}
74
+ {hasError && <p className="text-[9px] text-red-500 px-1 py-0.5">Failed to load columns</p>}
75
+ {columns && columns.map((col) => (
76
+ <div key={col.name} className="flex items-center gap-1 px-1 py-px text-[10px] text-text-subtle" title={col.fk ? `FK → ${col.fk.table}.${col.fk.column}` : undefined}>
77
+ {col.pk && <Key className="size-2.5 text-amber-500 shrink-0" />}
78
+ {col.fk && <Link2 className="size-2.5 text-blue-400 shrink-0" />}
79
+ {!col.pk && !col.fk && <span className="size-2.5 shrink-0" />}
80
+ <span className="truncate">{col.name}{col.nullable ? "?" : ""}</span>
81
+ <span className="ml-auto text-[9px] text-text-subtle/60 shrink-0">{col.type}</span>
82
+ </div>
83
+ ))}
84
+ {!isLoadingCols && !hasError && !columns && <p className="text-[9px] text-text-subtle px-1 py-0.5">No columns</p>}
85
+ </div>
86
+ )}
87
+ </div>
88
+ );
89
+ })}
90
+ </div>
91
+ );
92
+ })}
93
+ {filter && Array.from(schemas.values()).every((t) => !t.some((x) => x.tableName.toLowerCase().includes(filterLower))) && (
94
+ <p className="text-[10px] text-text-subtle px-2 py-1">No match</p>
95
+ )}
96
+ </div>
97
+ );
98
+ }
@@ -1,4 +1,4 @@
1
- import { useState, useEffect, useCallback } from "react";
1
+ import { useState, useEffect, useCallback, useRef } from "react";
2
2
  import { api } from "../../lib/api-client";
3
3
 
4
4
  export interface Connection {
@@ -104,17 +104,20 @@ export function useConnections() {
104
104
  }, []);
105
105
 
106
106
  /** Fetch column metadata for a table (lazy loaded for schema tree) */
107
- const [columnCache, setColumnCache] = useState<Map<string, { name: string; type: string; nullable: boolean; pk: boolean; fk: { table: string; column: string } | null }[]>>(new Map());
108
- const fetchColumns = useCallback(async (connId: number, table: string, schema?: string): Promise<{ name: string; type: string; nullable: boolean; pk: boolean; fk: { table: string; column: string } | null }[]> => {
107
+ type ColumnInfo = { name: string; type: string; nullable: boolean; pk: boolean; fk: { table: string; column: string } | null };
108
+ const [columnCache, setColumnCache] = useState<Map<string, ColumnInfo[]>>(new Map());
109
+ const columnCacheRef = useRef(columnCache);
110
+ columnCacheRef.current = columnCache;
111
+ const fetchColumns = useCallback(async (connId: number, table: string, schema?: string): Promise<ColumnInfo[]> => {
109
112
  const cacheKey = `${connId}:${schema ?? "main"}.${table}`;
110
- const cached = columnCache.get(cacheKey);
113
+ const cached = columnCacheRef.current.get(cacheKey);
111
114
  if (cached) return cached;
112
- const cols = await api.get<{ name: string; type: string; nullable: boolean; pk: boolean; fk: { table: string; column: string } | null }[]>(
115
+ const cols = await api.get<ColumnInfo[]>(
113
116
  `/api/db/connections/${connId}/schema?table=${encodeURIComponent(table)}${schema ? `&schema=${encodeURIComponent(schema)}` : ""}`,
114
117
  );
115
118
  setColumnCache((prev) => new Map(prev).set(cacheKey, cols));
116
119
  return cols;
117
- }, [columnCache]);
120
+ }, []);
118
121
 
119
122
  const exportConnections = useCallback(async () => {
120
123
  return api.get<{ version: number; exported_at: string; connections: unknown[] }>("/api/db/connections/export");
@@ -300,7 +300,7 @@ export function useChat(sessionId: string | null, providerId = "claude", project
300
300
  });
301
301
  if (sessionIdRef.current && !isSessionTabActive(sessionIdRef.current)) {
302
302
  const nType = ev.tool === "AskUserQuestion" ? "question" : "approval_request";
303
- useNotificationStore.getState().addNotification(sessionIdRef.current, nType, projectNameRef.current);
303
+ // Unread state added via server-side session:unread_changed broadcast — only play sound + toast here
304
304
  playNotificationSound(nType);
305
305
  // Persistent toast with action to navigate to the waiting session
306
306
  const sid = sessionIdRef.current;
@@ -417,7 +417,7 @@ export function useChat(sessionId: string | null, providerId = "claude", project
417
417
  setContextWindowPct(ev.contextWindowPct);
418
418
  }
419
419
  if (sessionIdRef.current && !isSessionTabActive(sessionIdRef.current)) {
420
- useNotificationStore.getState().addNotification(sessionIdRef.current, "done", projectNameRef.current);
420
+ // Unread state added via server-side session:unread_changed broadcast — only play sound here
421
421
  playNotificationSound("done");
422
422
  }
423
423
  // Cancel any pending throttled sync — done handler writes final state directly
@@ -481,6 +481,13 @@ export function useChat(sessionId: string | null, providerId = "claude", project
481
481
  return;
482
482
  }
483
483
 
484
+ // Cross-tab/device unread sync — server broadcasts when unread state changes
485
+ if ((data as any).type === "session:unread_changed") {
486
+ const { sessionId: sid, unreadCount, unreadType, projectName: pn } = data as any;
487
+ useNotificationStore.getState().handleUnreadChanged(sid, unreadCount, unreadType, pn);
488
+ return;
489
+ }
490
+
484
491
  // Dispatch global Jira events so components can listen via window events
485
492
  if (typeof (data as any).type === "string" && (data as any).type.startsWith("jira:")) {
486
493
  window.dispatchEvent(new CustomEvent((data as any).type, { detail: data }));
@@ -0,0 +1,10 @@
1
+ import { useState, useEffect } from "react";
2
+
3
+ export function useDebouncedValue<T>(value: T, delayMs: number): T {
4
+ const [debounced, setDebounced] = useState(value);
5
+ useEffect(() => {
6
+ const timer = setTimeout(() => setDebounced(value), delayMs);
7
+ return () => clearTimeout(timer);
8
+ }, [value, delayMs]);
9
+ return debounced;
10
+ }
@@ -1,4 +1,5 @@
1
1
  import { create } from "zustand";
2
+ import { api } from "@/lib/api-client";
2
3
 
3
4
  interface NotificationEntry {
4
5
  count: number;
@@ -31,6 +32,10 @@ interface NotificationStore {
31
32
  addNotification: (sessionId: string, type: string, projectName: string) => void;
32
33
  clearForSession: (sessionId: string) => void;
33
34
  clearAll: () => void;
35
+ /** Hydrate from backend on app load */
36
+ loadFromServer: (projectName: string) => Promise<void>;
37
+ /** Handle WS broadcast for cross-tab/device sync */
38
+ handleUnreadChanged: (sessionId: string, unreadCount: number, unreadType: string | null, projectName: string) => void;
34
39
  }
35
40
 
36
41
  export const useNotificationStore = create<NotificationStore>()((set) => ({
@@ -56,9 +61,46 @@ export const useNotificationStore = create<NotificationStore>()((set) => ({
56
61
  next.delete(sessionId);
57
62
  return { notifications: next };
58
63
  });
64
+ // Fire-and-forget: persist to server so other tabs/devices sync
65
+ api.post(`/api/chat/sessions/${encodeURIComponent(sessionId)}/read`).catch(() => {});
59
66
  },
60
67
 
61
68
  clearAll: () => set({ notifications: new Map() }),
69
+
70
+ loadFromServer: async (projectName: string) => {
71
+ try {
72
+ const entries = await api.get<Array<{ sessionId: string; unreadCount: number; unreadType: string | null; projectName: string | null }>>(
73
+ `/api/project/${encodeURIComponent(projectName)}/chat/sessions/unread`,
74
+ );
75
+ set(() => {
76
+ const next = new Map<string, NotificationEntry>();
77
+ for (const e of entries) {
78
+ if (e.unreadCount > 0) {
79
+ next.set(e.sessionId, { count: e.unreadCount, type: e.unreadType || "done", projectName: e.projectName || "" });
80
+ }
81
+ }
82
+ return { notifications: next };
83
+ });
84
+ } catch { /* server may not support yet — keep empty */ }
85
+ },
86
+
87
+ handleUnreadChanged: (sessionId, unreadCount, unreadType, projectName) => {
88
+ set((state) => {
89
+ const next = new Map(state.notifications);
90
+ if (unreadCount === 0) {
91
+ next.delete(sessionId);
92
+ } else {
93
+ // unreadCount === -1 means "incremented, re-fetch actual count not available" — just +1 locally
94
+ const existing = next.get(sessionId);
95
+ next.set(sessionId, {
96
+ count: unreadCount > 0 ? unreadCount : (existing?.count ?? 0) + 1,
97
+ type: unreadType || "done",
98
+ projectName: projectName || existing?.projectName || "",
99
+ });
100
+ }
101
+ return { notifications: next };
102
+ });
103
+ },
62
104
  }));
63
105
 
64
106
  /** Derived: total unread count across all sessions */
@@ -1 +0,0 @@
1
- import{G as e}from"./vendor-mermaid-CPtQ2zua.js";export{e as createArchitectureServices};