@hienlh/ppm 0.8.63 → 0.8.65
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.
- package/CHANGELOG.md +21 -0
- package/bun.lock +5 -0
- package/dist/web/assets/{_basePickBy-CZovQgWd.js → _basePickBy-CrBYAu1z.js} +1 -1
- package/dist/web/assets/{_baseUniq-ClnvscgW.js → _baseUniq-PMQMCyd4.js} +1 -1
- package/dist/web/assets/{api-settings--eVrUeZM.js → api-settings-Cj0OeEMH.js} +1 -1
- package/dist/web/assets/{arc-C2Qaz-ch.js → arc-Bx6TKJWg.js} +1 -1
- package/dist/web/assets/architecture-PBZL5I3N-DyGQgfy0.js +1 -0
- package/dist/web/assets/{architectureDiagram-2XIMDMQ5-Jq91S_rs.js → architectureDiagram-2XIMDMQ5-Dinq4Kbf.js} +1 -1
- package/dist/web/assets/arrow-up--LjUXLEt.js +1 -0
- package/dist/web/assets/{blockDiagram-WCTKOSBZ-CKGufRTy.js → blockDiagram-WCTKOSBZ-CXh2T4Mt.js} +1 -1
- package/dist/web/assets/{browser-tab-CjjWgPDL.js → browser-tab-BT8iJmIO.js} +1 -1
- package/dist/web/assets/{c4Diagram-IC4MRINW-BNP2L9r_.js → c4Diagram-IC4MRINW-BCqRHxU0.js} +1 -1
- package/dist/web/assets/channel-GxJZ6KAL.js +1 -0
- package/dist/web/assets/chat-tab-B5tNFtvt.js +7 -0
- package/dist/web/assets/chevron-right-DeV0ehiG.js +1 -0
- package/dist/web/assets/{chunk-4BX2VUAB-BptTlTyl.js → chunk-4BX2VUAB-BNc3BCto.js} +1 -1
- package/dist/web/assets/{chunk-55IACEB6-C4mUdyio.js → chunk-55IACEB6-NTU3iwXh.js} +1 -1
- package/dist/web/assets/{chunk-7E7YKBS2-6xAQfBwa.js → chunk-7E7YKBS2-B3LDS0kJ.js} +1 -1
- package/dist/web/assets/{chunk-7R4GIKGN-DXaGAn_K.js → chunk-7R4GIKGN-Y5Bg-bKJ.js} +2 -2
- package/dist/web/assets/{chunk-C72U2L5F-DOtEiN5f.js → chunk-C72U2L5F-0CMBXF9Y.js} +1 -1
- package/dist/web/assets/{chunk-EGIJ26TM-D0KJTa_T.js → chunk-EGIJ26TM-BATHdF9E.js} +1 -1
- package/dist/web/assets/{chunk-FMBD7UC4-C_1aG0eb.js → chunk-FMBD7UC4-BrH-Lnio.js} +1 -1
- package/dist/web/assets/{chunk-GEFDOKGD-DwVPiYfW.js → chunk-GEFDOKGD-_UEQ_Hqk.js} +1 -1
- package/dist/web/assets/chunk-GLR3WWYH-BWjkRp6S.js +2 -0
- package/dist/web/assets/chunk-HHEYEP7N-DhIRtWNx.js +1 -0
- package/dist/web/assets/{chunk-JSJVCQXG-BSrqCL_3.js → chunk-JSJVCQXG-LNOdgWAh.js} +1 -1
- package/dist/web/assets/{chunk-KX2RTZJC-BCxGmbzy.js → chunk-KX2RTZJC-AniEyPkm.js} +1 -1
- package/dist/web/assets/{chunk-KYZI473N-BKO5gMeU.js → chunk-KYZI473N-drU3HQZZ.js} +1 -1
- package/dist/web/assets/{chunk-L3YUKLVL-3wBgkSvL.js → chunk-L3YUKLVL-GJ3l4AXc.js} +1 -1
- package/dist/web/assets/{chunk-MX3YWQON-BgjSEzus.js → chunk-MX3YWQON-OOf6bZzK.js} +1 -1
- package/dist/web/assets/{chunk-NQ4KR5QH-DLrZwBEm.js → chunk-NQ4KR5QH-DoAhAAPR.js} +1 -1
- package/dist/web/assets/{chunk-O4XLMI2P-BurQy8tt.js → chunk-O4XLMI2P-BBFcNFB_.js} +1 -1
- package/dist/web/assets/{chunk-OZEHJAEY-YTn24bGg.js → chunk-OZEHJAEY-CxBlPGUC.js} +1 -1
- package/dist/web/assets/{chunk-PQ6SQG4A-BxtUGYhW.js → chunk-PQ6SQG4A-DZGwvbzy.js} +1 -1
- package/dist/web/assets/{chunk-PU5JKC2W-B66ELkQm.js → chunk-PU5JKC2W-CCXYoac7.js} +1 -1
- package/dist/web/assets/chunk-QZHKN3VN-CzG4AaJv.js +1 -0
- package/dist/web/assets/{chunk-R5LLSJPH-euR2RxLN.js → chunk-R5LLSJPH-BTcrDJ0u.js} +1 -1
- package/dist/web/assets/{chunk-WL4C6EOR-_2CBOJdI.js → chunk-WL4C6EOR-BkioZxSz.js} +1 -1
- package/dist/web/assets/{chunk-XIRO2GV7-kqQ0g6wW.js → chunk-XIRO2GV7-BojyzPX8.js} +1 -1
- package/dist/web/assets/{chunk-XPW4576I-CtcaMb09.js → chunk-XPW4576I-no3g3yPs.js} +1 -1
- package/dist/web/assets/{chunk-XZSTWKYB-BYxFzZwS.js → chunk-XZSTWKYB-DTNisBJo.js} +1 -1
- package/dist/web/assets/{chunk-YBOYWFTD-Dx_fX35n.js → chunk-YBOYWFTD-DGKGjR6H.js} +1 -1
- package/dist/web/assets/classDiagram-VBA2DB6C-D3WeZMpV.js +1 -0
- package/dist/web/assets/classDiagram-v2-RAHNMMFH-CuONv6uF.js +1 -0
- package/dist/web/assets/clone-BGvnD7V5.js +1 -0
- package/dist/web/assets/code-editor-4UczMI-T.js +2 -0
- package/dist/web/assets/{cose-bilkent-S5V4N54A-CHHjH2dV.js → cose-bilkent-S5V4N54A-zoeAORzo.js} +1 -1
- package/dist/web/assets/csv-preview-CgHOY6hR.js +9 -0
- package/dist/web/assets/{dagre-CNtSxiE_.js → dagre-Bh_0hC-U.js} +1 -1
- package/dist/web/assets/{dagre-KLK3FWXG-ChenfPp1.js → dagre-KLK3FWXG-C5SfvcWS.js} +1 -1
- package/dist/web/assets/database-viewer-CUjlWuu_.js +1 -0
- package/dist/web/assets/{diagram-E7M64L7V-CzKYZM0Y.js → diagram-E7M64L7V-CZUkePtj.js} +1 -1
- package/dist/web/assets/{diagram-IFDJBPK2-ChB_paPo.js → diagram-IFDJBPK2-C_f4KWUQ.js} +1 -1
- package/dist/web/assets/{diagram-P4PSJMXO-D1eW1dkL.js → diagram-P4PSJMXO-DCIgnRzg.js} +1 -1
- package/dist/web/assets/diff-viewer-B4lDAzWv.js +4 -0
- package/dist/web/assets/dist-B21lOVUR.js +1 -0
- package/dist/web/assets/dist-DylI9XxN.js +13 -0
- package/dist/web/assets/dist-lF8CoYII.js +41 -0
- package/dist/web/assets/{erDiagram-INFDFZHY-mCvUFSn6.js → erDiagram-INFDFZHY-BmVZ3V1Q.js} +1 -1
- package/dist/web/assets/{flowDiagram-PKNHOUZH-14ohZ1M1.js → flowDiagram-PKNHOUZH-DQVk-izW.js} +1 -1
- package/dist/web/assets/{ganttDiagram-A5KZAMGK-DIX0pLbk.js → ganttDiagram-A5KZAMGK-BesueWL2.js} +1 -1
- package/dist/web/assets/git-graph-BTZTGZdP.js +1 -0
- package/dist/web/assets/gitGraph-HDMCJU4V-sd3XYN52.js +1 -0
- package/dist/web/assets/{gitGraphDiagram-K3NZZRJ6-yEWZbdf_.js → gitGraphDiagram-K3NZZRJ6-BnKRAgIF.js} +1 -1
- package/dist/web/assets/{graphlib-DhOZxqsh.js → graphlib-BZkhmhks.js} +1 -1
- package/dist/web/assets/index-DPI-YVJI.css +2 -0
- package/dist/web/assets/index-hxAJJHpg.js +37 -0
- package/dist/web/assets/info-3K5VOQVL-DdzKhhLF.js +1 -0
- package/dist/web/assets/infoDiagram-LFFYTUFH-CnHorrOf.js +2 -0
- package/dist/web/assets/input-Bid961xi.js +1 -0
- package/dist/web/assets/{isEmpty-C0YYdhYj.js → isEmpty-CauZBoL-.js} +1 -1
- package/dist/web/assets/{ishikawaDiagram-PHBUUO56-olazD6dZ.js → ishikawaDiagram-PHBUUO56-Proo1Zyp.js} +1 -1
- package/dist/web/assets/{journeyDiagram-4ABVD52K-CttDH9bb.js → journeyDiagram-4ABVD52K-86MYRgHA.js} +1 -1
- package/dist/web/assets/{kanban-definition-K7BYSVSG-BBXbI37U.js → kanban-definition-K7BYSVSG-lsZmnsv1.js} +1 -1
- package/dist/web/assets/keybindings-store-BHUZp2i1.js +1 -0
- package/dist/web/assets/lib-Dmwceoh0.js +4 -0
- package/dist/web/assets/{line-DBLLF7lH.js → line-CNR7Z1Dm.js} +1 -1
- package/dist/web/assets/{linear-BLFWatDe.js → linear-CGDVbc3k.js} +1 -1
- package/dist/web/assets/markdown-renderer-ljVdhEEq.js +69 -0
- package/dist/web/assets/{mermaid-parser.core-BKiGOTjR.js → mermaid-parser.core-C3bmPvHW.js} +2 -2
- package/dist/web/assets/{mindmap-definition-YRQLILUH-DoT7m4Sz.js → mindmap-definition-YRQLILUH-BqZbwYh4.js} +1 -1
- package/dist/web/assets/{ordinal-CCj7PWgZ.js → ordinal-D8VwtNz9.js} +1 -1
- package/dist/web/assets/packet-RMMSAZCW-BJANCvjY.js +1 -0
- package/dist/web/assets/pie-UPGHQEXC-DM2KFfMP.js +1 -0
- package/dist/web/assets/{pieDiagram-SKSYHLDU-Bkh2E4zE.js → pieDiagram-SKSYHLDU-CysZmHF5.js} +1 -1
- package/dist/web/assets/postgres-viewer-Dlkyzslt.js +1 -0
- package/dist/web/assets/{quadrantDiagram-337W2JSQ-B7zgALOL.js → quadrantDiagram-337W2JSQ-B1KHH43M.js} +1 -1
- package/dist/web/assets/radar-KQ55EAFF-BB-ywBD0.js +1 -0
- package/dist/web/assets/react-dom-Bpkvzu3U.js +1 -0
- package/dist/web/assets/{requirementDiagram-Z7DCOOCP-D_5GXNRo.js → requirementDiagram-Z7DCOOCP-DpYZNbHV.js} +1 -1
- package/dist/web/assets/{sankeyDiagram-WA2Y5GQK-BA9EFAAe.js → sankeyDiagram-WA2Y5GQK-1QhDzoqT.js} +1 -1
- package/dist/web/assets/{sequenceDiagram-2WXFIKYE-fyWIrHiG.js → sequenceDiagram-2WXFIKYE-yoBahL9m.js} +1 -1
- package/dist/web/assets/settings-tab-BSfQh_HW.js +1 -0
- package/dist/web/assets/sqlite-viewer-DraX5cHB.js +1 -0
- package/dist/web/assets/{stateDiagram-RAJIS63D-DfRBcaBu.js → stateDiagram-RAJIS63D-c6UuHXom.js} +1 -1
- package/dist/web/assets/stateDiagram-v2-FVOUBMTO-0R9EtHx1.js +1 -0
- package/dist/web/assets/{tab-store-DcIBZTD4.js → tab-store-Ck-9RsH-.js} +1 -1
- package/dist/web/assets/{terminal-tab-B0TAHXjw.js → terminal-tab-C_jOKXBm.js} +2 -2
- package/dist/web/assets/{timeline-definition-YZTLITO2-DYfwJ1jM.js → timeline-definition-YZTLITO2-BayV-4fk.js} +1 -1
- package/dist/web/assets/treemap-KZPCXAKY-DGHYomZ7.js +1 -0
- package/dist/web/assets/use-monaco-theme-BHp8EuUZ.js +11 -0
- package/dist/web/assets/{vennDiagram-LZ73GAT5-DqbKNRD9.js → vennDiagram-LZ73GAT5-B_s6kymD.js} +1 -1
- package/dist/web/assets/{xychartDiagram-JWTSCODW-DhUL86qT.js → xychartDiagram-JWTSCODW-PwVoWxs2.js} +1 -1
- package/dist/web/index.html +13 -10
- package/dist/web/sw.js +1 -1
- package/docs/codebase-summary.md +9 -3
- package/docs/design-guidelines.md +21 -0
- package/package.json +2 -1
- package/src/providers/claude-agent-sdk.ts +14 -2
- package/src/server/index.ts +4 -0
- package/src/server/routes/browser-preview.ts +2 -2
- package/src/server/routes/chat.ts +4 -2
- package/src/server/ws/chat.ts +12 -6
- package/src/services/account-selector.service.ts +8 -2
- package/src/services/claude-usage.service.ts +37 -11
- package/src/services/db.service.ts +45 -6
- package/src/web/components/editor/code-editor.tsx +36 -26
- package/src/web/components/editor/csv-preview.tsx +207 -0
- package/src/web/components/editor/editor-breadcrumb.tsx +225 -0
- package/src/web/components/editor/editor-toolbar.tsx +74 -0
- package/src/web/lib/csv-parser.ts +134 -0
- package/dist/web/assets/architecture-PBZL5I3N-ChOahOB7.js +0 -1
- package/dist/web/assets/channel-w7yboq56.js +0 -1
- package/dist/web/assets/chat-tab--hD0r5RS.js +0 -7
- package/dist/web/assets/chunk-GLR3WWYH-D9pZakqr.js +0 -2
- package/dist/web/assets/chunk-HHEYEP7N-Dld5BpGB.js +0 -1
- package/dist/web/assets/chunk-QZHKN3VN-DwSXwtjH.js +0 -1
- package/dist/web/assets/classDiagram-VBA2DB6C-BpJ6Oog2.js +0 -1
- package/dist/web/assets/classDiagram-v2-RAHNMMFH-Bj8gIhkP.js +0 -1
- package/dist/web/assets/clone-BSi6cgDh.js +0 -1
- package/dist/web/assets/code-editor-EG9sb3gL.js +0 -1
- package/dist/web/assets/database-viewer-h1Zb9cFF.js +0 -1
- package/dist/web/assets/diff-viewer-DrTqG6RM.js +0 -4
- package/dist/web/assets/dist-T0Vhi0Mh.js +0 -16
- package/dist/web/assets/git-graph-Bx3h7BK1.js +0 -1
- package/dist/web/assets/gitGraph-HDMCJU4V-CEee2FCA.js +0 -1
- package/dist/web/assets/index-Beb248lR.css +0 -2
- package/dist/web/assets/index-DmfRyMpE.js +0 -37
- package/dist/web/assets/info-3K5VOQVL-ce_pi3En.js +0 -1
- package/dist/web/assets/infoDiagram-LFFYTUFH-BzqyoqXw.js +0 -2
- package/dist/web/assets/input-Brjz2Vv-.js +0 -41
- package/dist/web/assets/keybindings-store-BScuugqK.js +0 -1
- package/dist/web/assets/markdown-renderer-DwmzGpNI.js +0 -69
- package/dist/web/assets/packet-RMMSAZCW-CdYSLjRL.js +0 -1
- package/dist/web/assets/pie-UPGHQEXC-Bm5LiD-6.js +0 -1
- package/dist/web/assets/postgres-viewer-Bp6mOne8.js +0 -1
- package/dist/web/assets/radar-KQ55EAFF-C4PnyG7_.js +0 -1
- package/dist/web/assets/settings-store-Bbhg_ptG.js +0 -2
- package/dist/web/assets/settings-tab-8lfbaK4W.js +0 -1
- package/dist/web/assets/sqlite-viewer-Cenucoym.js +0 -1
- package/dist/web/assets/stateDiagram-v2-FVOUBMTO-CMN4M2Em.js +0 -1
- package/dist/web/assets/treemap-KZPCXAKY-2_y-mhkz.js +0 -1
- package/dist/web/assets/use-monaco-theme-vwto-Vlf.js +0 -11
- /package/dist/web/assets/{api-client-DpGMOZNf.js → api-client-DniUYaIY.js} +0 -0
- /package/dist/web/assets/{array-BGFCBI0e.js → array-Bxr4Uw0G.js} +0 -0
- /package/dist/web/assets/{columns-2-ChOTgl3e.js → columns-2-DpsNbZOc.js} +0 -0
- /package/dist/web/assets/{cytoscape.esm-Ccan6xou.js → cytoscape.esm-zgx_RusX.js} +0 -0
- /package/dist/web/assets/{defaultLocale-CRZydyG6.js → defaultLocale-BbBFINez.js} +0 -0
- /package/dist/web/assets/{dist-Cce3efmT.js → dist-CBQJF0q0.js} +0 -0
- /package/dist/web/assets/{init-B8gtcn7T.js → init-C9S7-8pU.js} +0 -0
- /package/dist/web/assets/{isArrayLikeObject-B4pdpV8V.js → isArrayLikeObject-sCNOrnc2.js} +0 -0
- /package/dist/web/assets/{katex-Bbu770d9.js → katex-C10ndCVt.js} +0 -0
- /package/dist/web/assets/{math-DwgHI-Cu.js → math-DO7stL4-.js} +0 -0
- /package/dist/web/assets/{path-DZF-JdEe.js → path-DPMyyJeP.js} +0 -0
- /package/dist/web/assets/{preload-helper-qlgyTAkD.js → preload-helper-uTix4PVD.js} +0 -0
- /package/dist/web/assets/{react-BGf7KNLk.js → react-ER-4DN55.js} +0 -0
- /package/dist/web/assets/{rough.esm-VLpapkIG.js → rough.esm-BWqoIFNR.js} +0 -0
- /package/dist/web/assets/{src-BoSBNdA_.js → src-BYKCXnff.js} +0 -0
- /package/dist/web/assets/{table-Yo02WRH-.js → table-C7X5UAEI.js} +0 -0
- /package/dist/web/assets/{tag-CaC1ng2E.js → tag-CCtdV063.js} +0 -0
- /package/dist/web/assets/{utils-btZ8C8-R.js → utils-BcFEFg4m.js} +0 -0
|
@@ -57,8 +57,14 @@ class AccountSelectorService {
|
|
|
57
57
|
// Clear expired cooldowns
|
|
58
58
|
for (const acc of allAccounts) {
|
|
59
59
|
if (acc.status === "cooldown" && acc.cooldownUntil && acc.cooldownUntil <= now) {
|
|
60
|
-
|
|
61
|
-
|
|
60
|
+
try {
|
|
61
|
+
accountService.setEnabled(acc.id);
|
|
62
|
+
this.retryCounts.delete(acc.id);
|
|
63
|
+
} catch {
|
|
64
|
+
// Account expired or cannot be re-enabled — disable it
|
|
65
|
+
accountService.setDisabled(acc.id);
|
|
66
|
+
this.retryCounts.delete(acc.id);
|
|
67
|
+
}
|
|
62
68
|
}
|
|
63
69
|
}
|
|
64
70
|
|
|
@@ -51,6 +51,10 @@ let pollTimer: ReturnType<typeof setInterval> | null = null;
|
|
|
51
51
|
|
|
52
52
|
// Per-token cooldown map: token prefix → earliest allowed fetch time
|
|
53
53
|
const tokenCooldowns = new Map<string, number>();
|
|
54
|
+
const MIN_COOLDOWN_MS = 60_000; // floor: at least 60s cooldown on 429
|
|
55
|
+
|
|
56
|
+
// Dedup: if a poll is already in-flight, reuse the same promise
|
|
57
|
+
let inflightPoll: Promise<void> | null = null;
|
|
54
58
|
|
|
55
59
|
// Legacy: Keychain token cache for users without accounts in DB
|
|
56
60
|
let tokenCache: { token: string; timestamp: number } | null = null;
|
|
@@ -113,9 +117,10 @@ async function fetchUsageForToken(token: string): Promise<ClaudeUsage> {
|
|
|
113
117
|
});
|
|
114
118
|
if (res.status === 429) {
|
|
115
119
|
const retryAfter = parseInt(res.headers.get("retry-after") ?? "60", 10);
|
|
120
|
+
const cooldownMs = Math.max(retryAfter * 1000, MIN_COOLDOWN_MS);
|
|
116
121
|
const cooldownKey = token.substring(0, 20);
|
|
117
|
-
tokenCooldowns.set(cooldownKey, Date.now() +
|
|
118
|
-
throw new Error(`Usage API 429 — cooldown ${
|
|
122
|
+
tokenCooldowns.set(cooldownKey, Date.now() + cooldownMs);
|
|
123
|
+
throw new Error(`Usage API 429 — cooldown ${Math.ceil(cooldownMs / 1000)}s`);
|
|
119
124
|
}
|
|
120
125
|
if (!res.ok) throw new Error(`Usage API returned ${res.status}`);
|
|
121
126
|
const raw = (await res.json()) as Record<string, any>;
|
|
@@ -228,7 +233,7 @@ async function fetchLegacySingleAccount(): Promise<void> {
|
|
|
228
233
|
} catch {}
|
|
229
234
|
}
|
|
230
235
|
|
|
231
|
-
async function
|
|
236
|
+
async function pollOnceInternal(): Promise<void> {
|
|
232
237
|
try {
|
|
233
238
|
const hasAccounts = accountService.list().length > 0;
|
|
234
239
|
if (hasAccounts) {
|
|
@@ -241,6 +246,18 @@ async function pollOnce(): Promise<void> {
|
|
|
241
246
|
}
|
|
242
247
|
}
|
|
243
248
|
|
|
249
|
+
/** Deduped: concurrent callers share a single in-flight fetch */
|
|
250
|
+
async function pollOnce(): Promise<void> {
|
|
251
|
+
if (inflightPoll) return inflightPoll;
|
|
252
|
+
const thisPoll = pollOnceInternal().finally(() => {
|
|
253
|
+
// Only clear if still the current poll — prevents a stale .finally() from
|
|
254
|
+
// clearing a newer poll after timeout handler force-nulled inflightPoll.
|
|
255
|
+
if (inflightPoll === thisPoll) inflightPoll = null;
|
|
256
|
+
});
|
|
257
|
+
inflightPoll = thisPoll;
|
|
258
|
+
return thisPoll;
|
|
259
|
+
}
|
|
260
|
+
|
|
244
261
|
// ---------------------------------------------------------------------------
|
|
245
262
|
// Public API
|
|
246
263
|
// ---------------------------------------------------------------------------
|
|
@@ -301,14 +318,14 @@ export function startUsagePolling(): void {
|
|
|
301
318
|
const POLL_TIMEOUT = 60_000; // max 60s per poll iteration
|
|
302
319
|
const scheduleNext = () => {
|
|
303
320
|
pollTimer = setTimeout(async () => {
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
321
|
+
const timeout = new Promise<"timeout">(r => setTimeout(() => r("timeout"), POLL_TIMEOUT));
|
|
322
|
+
const result = await Promise.race([
|
|
323
|
+
pollOnce().then(() => "done" as const),
|
|
324
|
+
timeout,
|
|
325
|
+
]).catch(() => "error" as const);
|
|
326
|
+
// If the poll timed out, force-clear inflightPoll so next scheduled poll
|
|
327
|
+
// starts a fresh fetch instead of reusing the stale hanging promise.
|
|
328
|
+
if (result === "timeout") inflightPoll = null;
|
|
312
329
|
scheduleNext();
|
|
313
330
|
}, POLL_INTERVAL);
|
|
314
331
|
};
|
|
@@ -327,3 +344,12 @@ export async function refreshUsageNow(): Promise<ClaudeUsage & { activeAccountId
|
|
|
327
344
|
await pollOnce();
|
|
328
345
|
return getCachedUsage();
|
|
329
346
|
}
|
|
347
|
+
|
|
348
|
+
/** @internal Test-only: reset module-level state between tests */
|
|
349
|
+
export function _resetForTesting(): void {
|
|
350
|
+
inMemoryCostUsd = 0;
|
|
351
|
+
if (pollTimer) { clearTimeout(pollTimer); pollTimer = null; }
|
|
352
|
+
tokenCooldowns.clear();
|
|
353
|
+
inflightPoll = null;
|
|
354
|
+
tokenCache = null;
|
|
355
|
+
}
|
|
@@ -4,7 +4,7 @@ import { homedir } from "node:os";
|
|
|
4
4
|
import { mkdirSync, existsSync } from "node:fs";
|
|
5
5
|
|
|
6
6
|
const PPM_DIR = process.env.PPM_HOME || resolve(homedir(), ".ppm");
|
|
7
|
-
const CURRENT_SCHEMA_VERSION =
|
|
7
|
+
const CURRENT_SCHEMA_VERSION = 8;
|
|
8
8
|
|
|
9
9
|
let db: Database | null = null;
|
|
10
10
|
let dbProfile: string | null = null;
|
|
@@ -228,6 +228,18 @@ function runMigrations(database: Database): void {
|
|
|
228
228
|
}
|
|
229
229
|
database.exec(`PRAGMA user_version = 7`);
|
|
230
230
|
}
|
|
231
|
+
|
|
232
|
+
if (current < 8) {
|
|
233
|
+
database.exec(`
|
|
234
|
+
CREATE TABLE IF NOT EXISTS session_titles (
|
|
235
|
+
session_id TEXT PRIMARY KEY,
|
|
236
|
+
title TEXT NOT NULL,
|
|
237
|
+
updated_at TEXT DEFAULT (datetime('now'))
|
|
238
|
+
);
|
|
239
|
+
|
|
240
|
+
PRAGMA user_version = 8;
|
|
241
|
+
`);
|
|
242
|
+
}
|
|
231
243
|
}
|
|
232
244
|
|
|
233
245
|
// ---------------------------------------------------------------------------
|
|
@@ -311,6 +323,33 @@ export function getAllSessionMappings(): Record<string, string> {
|
|
|
311
323
|
return result;
|
|
312
324
|
}
|
|
313
325
|
|
|
326
|
+
// ---------------------------------------------------------------------------
|
|
327
|
+
// Session title helpers (user-set titles persisted in PPM DB)
|
|
328
|
+
// ---------------------------------------------------------------------------
|
|
329
|
+
|
|
330
|
+
export function getSessionTitle(sessionId: string): string | null {
|
|
331
|
+
const row = getDb().query("SELECT title FROM session_titles WHERE session_id = ?").get(sessionId) as { title: string } | null;
|
|
332
|
+
return row?.title ?? null;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
export function setSessionTitle(sessionId: string, title: string): void {
|
|
336
|
+
getDb().query(
|
|
337
|
+
"INSERT INTO session_titles (session_id, title, updated_at) VALUES (?, ?, datetime('now')) ON CONFLICT(session_id) DO UPDATE SET title = excluded.title, updated_at = excluded.updated_at",
|
|
338
|
+
).run(sessionId, title);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/** Bulk-fetch DB titles for a list of session IDs. Returns map of id → title. */
|
|
342
|
+
export function getSessionTitles(sessionIds: string[]): Record<string, string> {
|
|
343
|
+
if (sessionIds.length === 0) return {};
|
|
344
|
+
const placeholders = sessionIds.map(() => "?").join(", ");
|
|
345
|
+
const rows = getDb().query(
|
|
346
|
+
`SELECT session_id, title FROM session_titles WHERE session_id IN (${placeholders})`,
|
|
347
|
+
).all(...sessionIds) as { session_id: string; title: string }[];
|
|
348
|
+
const result: Record<string, string> = {};
|
|
349
|
+
for (const r of rows) result[r.session_id] = r.title;
|
|
350
|
+
return result;
|
|
351
|
+
}
|
|
352
|
+
|
|
314
353
|
// ---------------------------------------------------------------------------
|
|
315
354
|
// Push subscription helpers
|
|
316
355
|
// ---------------------------------------------------------------------------
|
|
@@ -441,13 +480,13 @@ export function insertLimitSnapshot(data: Omit<LimitSnapshotRow, "id" | "recorde
|
|
|
441
480
|
|
|
442
481
|
export function getLatestLimitSnapshot(): LimitSnapshotRow | null {
|
|
443
482
|
return getDb().query(
|
|
444
|
-
"SELECT * FROM claude_limit_snapshots ORDER BY recorded_at DESC LIMIT 1",
|
|
483
|
+
"SELECT * FROM claude_limit_snapshots ORDER BY recorded_at DESC, id DESC LIMIT 1",
|
|
445
484
|
).get() as LimitSnapshotRow | null;
|
|
446
485
|
}
|
|
447
486
|
|
|
448
487
|
export function getLatestSnapshotForAccount(accountId: string): LimitSnapshotRow | null {
|
|
449
488
|
return getDb().query(
|
|
450
|
-
"SELECT * FROM claude_limit_snapshots WHERE account_id = ? ORDER BY recorded_at DESC LIMIT 1",
|
|
489
|
+
"SELECT * FROM claude_limit_snapshots WHERE account_id = ? ORDER BY recorded_at DESC, id DESC LIMIT 1",
|
|
451
490
|
).get(accountId) as LimitSnapshotRow | null;
|
|
452
491
|
}
|
|
453
492
|
|
|
@@ -455,17 +494,17 @@ export function getAllLatestSnapshots(): LimitSnapshotRow[] {
|
|
|
455
494
|
return getDb().query(
|
|
456
495
|
`SELECT s.* FROM claude_limit_snapshots s
|
|
457
496
|
INNER JOIN (
|
|
458
|
-
SELECT account_id, MAX(
|
|
497
|
+
SELECT account_id, MAX(id) as max_id
|
|
459
498
|
FROM claude_limit_snapshots WHERE account_id IS NOT NULL
|
|
460
499
|
GROUP BY account_id
|
|
461
|
-
) latest ON s.
|
|
500
|
+
) latest ON s.id = latest.max_id`,
|
|
462
501
|
).all() as LimitSnapshotRow[];
|
|
463
502
|
}
|
|
464
503
|
|
|
465
504
|
export function touchSnapshotTimestamp(accountId: string): void {
|
|
466
505
|
getDb().query(
|
|
467
506
|
`UPDATE claude_limit_snapshots SET recorded_at = datetime('now')
|
|
468
|
-
WHERE id = (SELECT id FROM claude_limit_snapshots WHERE account_id = ? ORDER BY recorded_at DESC LIMIT 1)`,
|
|
507
|
+
WHERE id = (SELECT id FROM claude_limit_snapshots WHERE account_id = ? ORDER BY recorded_at DESC, id DESC LIMIT 1)`,
|
|
469
508
|
).run(accountId);
|
|
470
509
|
}
|
|
471
510
|
|
|
@@ -7,7 +7,12 @@ import { useTabStore } from "@/stores/tab-store";
|
|
|
7
7
|
import { useSettingsStore } from "@/stores/settings-store";
|
|
8
8
|
import { basename } from "@/lib/utils";
|
|
9
9
|
import { useMonacoTheme } from "@/lib/use-monaco-theme";
|
|
10
|
-
import { Loader2, FileWarning, ExternalLink
|
|
10
|
+
import { Loader2, FileWarning, ExternalLink } from "lucide-react";
|
|
11
|
+
import { EditorBreadcrumb } from "./editor-breadcrumb";
|
|
12
|
+
import { EditorToolbar } from "./editor-toolbar";
|
|
13
|
+
import { lazy, Suspense } from "react";
|
|
14
|
+
|
|
15
|
+
const CsvPreview = lazy(() => import("./csv-preview").then((m) => ({ default: m.CsvPreview })));
|
|
11
16
|
|
|
12
17
|
/** Image extensions renderable inline */
|
|
13
18
|
const IMAGE_EXTS = new Set(["png", "jpg", "jpeg", "gif", "webp", "svg", "ico"]);
|
|
@@ -58,7 +63,9 @@ export function CodeEditor({ metadata, tabId }: CodeEditorProps) {
|
|
|
58
63
|
const isPdf = ext === "pdf";
|
|
59
64
|
const isSqlite = SQLITE_EXTS.has(ext);
|
|
60
65
|
const isMarkdown = ext === "md" || ext === "mdx";
|
|
66
|
+
const isCsv = ext === "csv";
|
|
61
67
|
const [mdMode, setMdMode] = useState<"edit" | "preview">("preview");
|
|
68
|
+
const [csvMode, setCsvMode] = useState<"table" | "raw">("table");
|
|
62
69
|
|
|
63
70
|
// Redirect .db files to sqlite viewer by changing tab type
|
|
64
71
|
useEffect(() => {
|
|
@@ -196,33 +203,36 @@ export function CodeEditor({ metadata, tabId }: CodeEditorProps) {
|
|
|
196
203
|
);
|
|
197
204
|
}
|
|
198
205
|
|
|
199
|
-
const mdModeButtons = isMarkdown ? (
|
|
200
|
-
<>
|
|
201
|
-
<button type="button" onClick={() => setMdMode("edit")}
|
|
202
|
-
className={`flex items-center gap-1 px-2 py-1 rounded text-xs transition-colors ${mdMode === "edit" ? "bg-muted text-foreground" : "text-muted-foreground hover:text-foreground"}`}
|
|
203
|
-
>
|
|
204
|
-
<Code className="size-3" /> Edit
|
|
205
|
-
</button>
|
|
206
|
-
<button type="button" onClick={() => setMdMode("preview")}
|
|
207
|
-
className={`flex items-center gap-1 px-2 py-1 rounded text-xs transition-colors ${mdMode === "preview" ? "bg-muted text-foreground" : "text-muted-foreground hover:text-foreground"}`}
|
|
208
|
-
>
|
|
209
|
-
<Eye className="size-3" /> Preview
|
|
210
|
-
</button>
|
|
211
|
-
</>
|
|
212
|
-
) : null;
|
|
213
|
-
|
|
214
|
-
const wrapBtn = (
|
|
215
|
-
<button type="button" onClick={toggleWordWrap} title="Toggle word wrap (Alt+Z)"
|
|
216
|
-
className={`flex items-center gap-1 px-2 py-1 rounded text-xs transition-colors ${wordWrap ? "bg-muted text-foreground" : "text-muted-foreground hover:text-foreground"}`}
|
|
217
|
-
>
|
|
218
|
-
<WrapText className="size-3" />
|
|
219
|
-
<span className="hidden sm:inline">Wrap</span>
|
|
220
|
-
</button>
|
|
221
|
-
);
|
|
222
|
-
|
|
223
206
|
return (
|
|
224
207
|
<div className="flex flex-col h-full w-full overflow-hidden">
|
|
225
|
-
{
|
|
208
|
+
{/* Breadcrumb + Toolbar bar — desktop only */}
|
|
209
|
+
{filePath && projectName && tabId && (
|
|
210
|
+
<div className="hidden md:flex items-center h-7 border-b border-border bg-background shrink-0">
|
|
211
|
+
<EditorBreadcrumb
|
|
212
|
+
filePath={filePath}
|
|
213
|
+
projectName={projectName}
|
|
214
|
+
tabId={tabId}
|
|
215
|
+
className="flex items-center flex-1 min-w-0 overflow-x-auto scrollbar-none px-2 gap-0.5"
|
|
216
|
+
/>
|
|
217
|
+
<EditorToolbar
|
|
218
|
+
ext={ext}
|
|
219
|
+
mdMode={mdMode}
|
|
220
|
+
onMdModeChange={setMdMode}
|
|
221
|
+
csvMode={csvMode}
|
|
222
|
+
onCsvModeChange={setCsvMode}
|
|
223
|
+
wordWrap={wordWrap}
|
|
224
|
+
onToggleWordWrap={toggleWordWrap}
|
|
225
|
+
className="shrink-0 flex items-center gap-1 px-2"
|
|
226
|
+
/>
|
|
227
|
+
</div>
|
|
228
|
+
)}
|
|
229
|
+
|
|
230
|
+
{/* Content area */}
|
|
231
|
+
{isCsv && csvMode === "table" ? (
|
|
232
|
+
<Suspense fallback={<div className="flex items-center justify-center h-full"><Loader2 className="size-5 animate-spin text-text-subtle" /></div>}>
|
|
233
|
+
<CsvPreview content={content ?? ""} onContentChange={handleChange} />
|
|
234
|
+
</Suspense>
|
|
235
|
+
) : isMarkdown && mdMode === "preview" ? (
|
|
226
236
|
<MarkdownPreview content={content ?? ""} />
|
|
227
237
|
) : (
|
|
228
238
|
<div className="flex-1 overflow-hidden">
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { useState, useMemo, useRef, useCallback, useEffect } from "react";
|
|
2
|
+
import {
|
|
3
|
+
useReactTable,
|
|
4
|
+
getCoreRowModel,
|
|
5
|
+
getSortedRowModel,
|
|
6
|
+
flexRender,
|
|
7
|
+
type ColumnDef,
|
|
8
|
+
type SortingState,
|
|
9
|
+
} from "@tanstack/react-table";
|
|
10
|
+
import { useVirtualizer } from "@tanstack/react-virtual";
|
|
11
|
+
import { parseCsv, serializeCsv } from "@/lib/csv-parser";
|
|
12
|
+
import { ArrowUp, ArrowDown } from "lucide-react";
|
|
13
|
+
|
|
14
|
+
interface CsvPreviewProps {
|
|
15
|
+
content: string;
|
|
16
|
+
onContentChange: (csv: string) => void;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function CsvPreview({ content, onContentChange }: CsvPreviewProps) {
|
|
20
|
+
const parsed = useMemo(() => parseCsv(content), [content]);
|
|
21
|
+
const [rows, setRows] = useState<string[][]>(() => parsed.rows);
|
|
22
|
+
const [sorting, setSorting] = useState<SortingState>([]);
|
|
23
|
+
const scrollRef = useRef<HTMLDivElement>(null);
|
|
24
|
+
const internalEditRef = useRef(false);
|
|
25
|
+
|
|
26
|
+
// Sync when content changes externally (e.g. file reload) — skip if we triggered it
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
if (internalEditRef.current) {
|
|
29
|
+
internalEditRef.current = false;
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
setRows(parsed.rows);
|
|
33
|
+
}, [parsed.rows]);
|
|
34
|
+
|
|
35
|
+
const headers = parsed.headers;
|
|
36
|
+
|
|
37
|
+
const updateCell = useCallback(
|
|
38
|
+
(rowIndex: number, colIndex: number, value: string) => {
|
|
39
|
+
setRows((prev) => {
|
|
40
|
+
const next = prev.map((r, i) => (i === rowIndex ? [...r] : r));
|
|
41
|
+
next[rowIndex]![colIndex] = value;
|
|
42
|
+
internalEditRef.current = true;
|
|
43
|
+
onContentChange(serializeCsv(headers, next));
|
|
44
|
+
return next;
|
|
45
|
+
});
|
|
46
|
+
},
|
|
47
|
+
[headers, onContentChange],
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
const columns = useMemo<ColumnDef<string[], string>[]>(
|
|
51
|
+
() =>
|
|
52
|
+
headers.map((h, i) => ({
|
|
53
|
+
id: `col-${i}`,
|
|
54
|
+
header: h || `Column ${i + 1}`,
|
|
55
|
+
accessorFn: (row: string[]) => row[i] ?? "",
|
|
56
|
+
cell: ({ row, getValue }) => (
|
|
57
|
+
<CsvCell
|
|
58
|
+
value={getValue()}
|
|
59
|
+
onSave={(v) => updateCell(row.index, i, v)}
|
|
60
|
+
/>
|
|
61
|
+
),
|
|
62
|
+
size: 150,
|
|
63
|
+
minSize: 80,
|
|
64
|
+
})),
|
|
65
|
+
[headers, updateCell],
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
const table = useReactTable({
|
|
69
|
+
data: rows,
|
|
70
|
+
columns,
|
|
71
|
+
state: { sorting },
|
|
72
|
+
onSortingChange: setSorting,
|
|
73
|
+
getCoreRowModel: getCoreRowModel(),
|
|
74
|
+
getSortedRowModel: getSortedRowModel(),
|
|
75
|
+
enableColumnResizing: true,
|
|
76
|
+
columnResizeMode: "onChange",
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
const { rows: tableRows } = table.getRowModel();
|
|
80
|
+
|
|
81
|
+
const virtualizer = useVirtualizer({
|
|
82
|
+
count: tableRows.length,
|
|
83
|
+
getScrollElement: () => scrollRef.current,
|
|
84
|
+
estimateSize: () => 32,
|
|
85
|
+
overscan: 20,
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
if (headers.length === 0) {
|
|
89
|
+
return (
|
|
90
|
+
<div className="flex items-center justify-center h-full text-muted-foreground text-sm">
|
|
91
|
+
Empty CSV file
|
|
92
|
+
</div>
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return (
|
|
97
|
+
<div ref={scrollRef} className="flex-1 overflow-auto">
|
|
98
|
+
<table className="w-full text-xs font-mono border-collapse">
|
|
99
|
+
<thead className="sticky top-0 bg-background z-10 border-b border-border block">
|
|
100
|
+
{table.getHeaderGroups().map((hg) => (
|
|
101
|
+
<tr key={hg.id} className="flex w-full">
|
|
102
|
+
{hg.headers.map((header) => (
|
|
103
|
+
<th
|
|
104
|
+
key={header.id}
|
|
105
|
+
className="relative text-left px-2 py-1.5 font-medium text-muted-foreground select-none cursor-pointer hover:bg-muted/50 border-r border-border last:border-r-0"
|
|
106
|
+
style={{ width: header.getSize(), minWidth: header.getSize() }}
|
|
107
|
+
onClick={header.column.getToggleSortingHandler()}
|
|
108
|
+
>
|
|
109
|
+
<div className="flex items-center gap-1">
|
|
110
|
+
<span className="truncate">
|
|
111
|
+
{flexRender(header.column.columnDef.header, header.getContext())}
|
|
112
|
+
</span>
|
|
113
|
+
{header.column.getIsSorted() === "asc" && <ArrowUp className="size-3 shrink-0" />}
|
|
114
|
+
{header.column.getIsSorted() === "desc" && <ArrowDown className="size-3 shrink-0" />}
|
|
115
|
+
</div>
|
|
116
|
+
{/* Resize handle */}
|
|
117
|
+
<div
|
|
118
|
+
onMouseDown={header.getResizeHandler()}
|
|
119
|
+
onTouchStart={header.getResizeHandler()}
|
|
120
|
+
onClick={(e) => e.stopPropagation()}
|
|
121
|
+
className="absolute right-0 top-0 h-full w-1 cursor-col-resize hover:bg-primary/50 active:bg-primary"
|
|
122
|
+
/>
|
|
123
|
+
</th>
|
|
124
|
+
))}
|
|
125
|
+
</tr>
|
|
126
|
+
))}
|
|
127
|
+
</thead>
|
|
128
|
+
<tbody style={{ height: virtualizer.getTotalSize(), position: "relative", display: "block" }}>
|
|
129
|
+
{virtualizer.getVirtualItems().map((vRow) => {
|
|
130
|
+
const row = tableRows[vRow.index]!;
|
|
131
|
+
return (
|
|
132
|
+
<tr
|
|
133
|
+
key={row.id}
|
|
134
|
+
data-index={vRow.index}
|
|
135
|
+
ref={(node) => virtualizer.measureElement(node)}
|
|
136
|
+
style={{
|
|
137
|
+
position: "absolute",
|
|
138
|
+
top: 0,
|
|
139
|
+
left: 0,
|
|
140
|
+
width: "100%",
|
|
141
|
+
transform: `translateY(${vRow.start}px)`,
|
|
142
|
+
display: "flex",
|
|
143
|
+
}}
|
|
144
|
+
>
|
|
145
|
+
{row.getVisibleCells().map((cell) => (
|
|
146
|
+
<td
|
|
147
|
+
key={cell.id}
|
|
148
|
+
className="px-2 py-1 border-b border-border/50 border-r border-r-border/30 last:border-r-0 truncate"
|
|
149
|
+
style={{ width: cell.column.getSize(), minWidth: cell.column.getSize() }}
|
|
150
|
+
>
|
|
151
|
+
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
|
152
|
+
</td>
|
|
153
|
+
))}
|
|
154
|
+
</tr>
|
|
155
|
+
);
|
|
156
|
+
})}
|
|
157
|
+
</tbody>
|
|
158
|
+
</table>
|
|
159
|
+
</div>
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function CsvCell({ value, onSave }: { value: string; onSave: (v: string) => void }) {
|
|
164
|
+
const [editing, setEditing] = useState(false);
|
|
165
|
+
const [draft, setDraft] = useState(value);
|
|
166
|
+
const inputRef = useRef<HTMLInputElement>(null);
|
|
167
|
+
|
|
168
|
+
useEffect(() => {
|
|
169
|
+
if (editing) inputRef.current?.focus();
|
|
170
|
+
}, [editing]);
|
|
171
|
+
|
|
172
|
+
if (!editing) {
|
|
173
|
+
return (
|
|
174
|
+
<span
|
|
175
|
+
className="block truncate cursor-text"
|
|
176
|
+
onClick={() => {
|
|
177
|
+
setDraft(value);
|
|
178
|
+
setEditing(true);
|
|
179
|
+
}}
|
|
180
|
+
>
|
|
181
|
+
{value || "\u00A0"}
|
|
182
|
+
</span>
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return (
|
|
187
|
+
<input
|
|
188
|
+
ref={inputRef}
|
|
189
|
+
className="w-full bg-transparent outline-none border-b border-primary text-xs font-mono"
|
|
190
|
+
value={draft}
|
|
191
|
+
onChange={(e) => setDraft(e.target.value)}
|
|
192
|
+
onBlur={() => {
|
|
193
|
+
setEditing(false);
|
|
194
|
+
if (draft !== value) onSave(draft);
|
|
195
|
+
}}
|
|
196
|
+
onKeyDown={(e) => {
|
|
197
|
+
if (e.key === "Enter") {
|
|
198
|
+
setEditing(false);
|
|
199
|
+
if (draft !== value) onSave(draft);
|
|
200
|
+
} else if (e.key === "Escape") {
|
|
201
|
+
setEditing(false);
|
|
202
|
+
setDraft(value);
|
|
203
|
+
}
|
|
204
|
+
}}
|
|
205
|
+
/>
|
|
206
|
+
);
|
|
207
|
+
}
|