@hienlh/ppm 0.22.0 → 0.22.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.
- package/CHANGELOG.md +20 -0
- package/CLAUDE.md +1 -1
- package/assets/skills/ppm/SKILL.md +1 -1
- package/assets/skills/ppm/references/cli-reference.md +4 -4
- package/assets/skills/ppm/references/http-api.md +1 -1
- package/dist/web/assets/accounts-settings-section-CLEAA_Jc.js +1 -0
- package/dist/web/assets/{ai-resource-editor-CdDpOwjh.js → ai-resource-editor-C4ecid2g.js} +1 -1
- package/dist/web/assets/ai-settings-section-BOwGdiyH.js +1 -0
- package/dist/web/assets/ai-settings-section-Cqz13gRt.js +1 -0
- package/dist/web/assets/{api-client-C1ZwRL4Y.js → api-client-Jn3njtNw.js} +1 -1
- package/dist/web/assets/api-client-rSFnnCj4.js +1 -0
- package/dist/web/assets/{api-settings-_7Q-ULxp.js → api-settings-BxijvPBR.js} +1 -1
- package/dist/web/assets/{api-themes-DRAERo4Q.js → api-themes-CSUarkzD.js} +1 -1
- package/dist/web/assets/{appearance-settings-section-DLoPK0LD.js → appearance-settings-section-DbA8L-Vz.js} +1 -1
- package/dist/web/assets/{audio-preview-DsYvgKqj.js → audio-preview-CxJd8wRV.js} +1 -1
- package/dist/web/assets/{branch-review-tab-G84Ojzae.js → branch-review-tab-CMTa2uSR.js} +1 -1
- package/dist/web/assets/chat-tab-BpdgqjSL.js +15 -0
- package/dist/web/assets/{code-editor-D04iUPsZ.js → code-editor-B7y7-5KE.js} +4 -4
- package/dist/web/assets/{codex-daily-guard-BSccLRGT.js → codex-daily-guard-ChDPiXDx.js} +1 -1
- package/dist/web/assets/{conflict-editor-C4ay676e.js → conflict-editor-Axs9Ad7A.js} +3 -3
- package/dist/web/assets/{csv-preview-DbwEmG7o.js → csv-preview-DTUAW2HW.js} +1 -1
- package/dist/web/assets/{database-viewer-S6LsALjX.js → database-viewer-qXDiWeXj.js} +1 -1
- package/dist/web/assets/{diff-viewer-3oBA7vgQ.js → diff-viewer-BtLiSpqe.js} +1 -1
- package/dist/web/assets/{docx-preview-CXuk-emm.js → docx-preview-Dxvy6h13.js} +1 -1
- package/dist/web/assets/{editor-language-service-DJmaKVY0.js → editor-language-service-D75GuLXP.js} +1 -1
- package/dist/web/assets/{explorer-body-BQ0jyAGv.js → explorer-body-xg1-MhlW.js} +2 -2
- package/dist/web/assets/{explorer-window-content-jdkWt34J.js → explorer-window-content-BeQocW3i.js} +1 -1
- package/dist/web/assets/extension-manager-section-Bm2v3P-n.js +1 -0
- package/dist/web/assets/{extension-webview-BGSDUbdK.js → extension-webview-CzxrAHyL.js} +1 -1
- package/dist/web/assets/{file-icons-BXdvggK2.js → file-icons-wS-OmEVu.js} +1 -1
- package/dist/web/assets/{file-store-ojgMXgAw.js → file-store-Ds3qJ4hC.js} +1 -1
- package/dist/web/assets/{files-settings-section-Gnyd7han.js → files-settings-section-Bd-_GeZU.js} +1 -1
- package/dist/web/assets/{format-file-meta-C77h04cZ.js → format-file-meta-PPi3ptkm.js} +1 -1
- package/dist/web/assets/{general-settings-section-V6Khh15V.js → general-settings-section-CfNPHQUk.js} +1 -1
- package/dist/web/assets/{git-log-panel-Bi513qjT.js → git-log-panel-CtGxPzH6.js} +1 -1
- package/dist/web/assets/{git-repo-store-88Z8v-pF.js → git-repo-store-BCEPfUGT.js} +1 -1
- package/dist/web/assets/{glide-data-grid-DnQwYSmQ.js → glide-data-grid-iSo2vP0q.js} +1 -1
- package/dist/web/assets/{group-chat-tab-DpczdwyV.js → group-chat-tab-D1lx0zz-.js} +1 -1
- package/dist/web/assets/{image-preview-CzCc4_Do.js → image-preview-DeGD0HJu.js} +1 -1
- package/dist/web/assets/index-CN0G8uYY.js +52 -0
- package/dist/web/assets/{jira-watcher-section-f2HUbGDY.js → jira-watcher-section-kyifZPTv.js} +1 -1
- package/dist/web/assets/keybindings-store-L5vV8DEJ.js +1 -0
- package/dist/web/assets/{keyboard-shortcuts-section-BpvVEw_Y.js → keyboard-shortcuts-section-D2uJq4x9.js} +1 -1
- package/dist/web/assets/markdown-renderer-BX1Q00sC.js +3 -0
- package/dist/web/assets/markdown-renderer-CLUux0vA.js +1 -0
- package/dist/web/assets/{media-url-yfqHpPz0.js → media-url-DKK0YHxL.js} +1 -1
- package/dist/web/assets/{mobile-explorer-sheet-Bol3XAO1.js → mobile-explorer-sheet-BHQDzpwK.js} +1 -1
- package/dist/web/assets/notification-store-BHgSy-ci.js +1 -0
- package/dist/web/assets/panel-store-D20Zuf3i.js +1 -0
- package/dist/web/assets/{pdf-preview-CmBI6E7d.js → pdf-preview-DAcfhfGm.js} +1 -1
- package/dist/web/assets/{postgres-viewer-8ZAqEALv.js → postgres-viewer-DPRsmgP5.js} +1 -1
- package/dist/web/assets/{ppmbot-settings-section-DKJUA3Kq.js → ppmbot-settings-section-B5N1jHgm.js} +1 -1
- package/dist/web/assets/{problems-panel-CFcoUx8h.js → problems-panel-D_wZ4_Qg.js} +1 -1
- package/dist/web/assets/{process-table-EKr2dqG6.js → process-table-DahflyQn.js} +1 -1
- package/dist/web/assets/{project-framework-store-B70Q3dNM.js → project-framework-store-ViZgce6y.js} +1 -1
- package/dist/web/assets/{project-store-B87hLRSk.js → project-store-hNvwfqX3.js} +1 -1
- package/dist/web/assets/{proxy-settings-section-B7qRVl8w.js → proxy-settings-section-CLg_nlxn.js} +1 -1
- package/dist/web/assets/{query-audit-section-By6qyqdB.js → query-audit-section-BrfSo0s_.js} +1 -1
- package/dist/web/assets/{remote-desktop-clipboard-notice-0WDYsyCA.js → remote-desktop-clipboard-notice-Fi6cieGM.js} +1 -1
- package/dist/web/assets/{remote-desktop-mobile-sheet-COYDg16H.js → remote-desktop-mobile-sheet-lLZ-oK73.js} +2 -2
- package/dist/web/assets/{remote-desktop-mobile-view-2DqSuS2U.js → remote-desktop-mobile-view-j3v-4rYa.js} +1 -1
- package/dist/web/assets/{remote-desktop-readiness-gate-Dm_qMJMH.js → remote-desktop-readiness-gate-CeLvcX_w.js} +1 -1
- package/dist/web/assets/{remote-desktop-window-content-UodhsF7W.js → remote-desktop-window-content-lwbkLsTC.js} +1 -1
- package/dist/web/assets/{run-in-terminal-CdefEe_R.js → run-in-terminal-D7W4IKb9.js} +1 -1
- package/dist/web/assets/{schedules-settings-section-bG2lYfGv.js → schedules-settings-section-C8OS0o80.js} +1 -1
- package/dist/web/assets/{send-to-chat-F207yH3u.js → send-to-chat-DNKhmHbi.js} +1 -1
- package/dist/web/assets/{settings-body-C06bFarv.js → settings-body-DgN22Z0W.js} +2 -2
- package/dist/web/assets/{settings-store-krC6bc79.js → settings-store-CsVSVgLp.js} +2 -2
- package/dist/web/assets/{settings-tab-34RSaASd.js → settings-tab-ChOs94iB.js} +1 -1
- package/dist/web/assets/{settings-window-content-Dr_2lmHH.js → settings-window-content-BnKMmg2a.js} +1 -1
- package/dist/web/assets/{sql-query-editor-BgRH0cHD.js → sql-query-editor-BJZIIQsm.js} +1 -1
- package/dist/web/assets/{sqlite-viewer-DUxGslpa.js → sqlite-viewer--cMeCZum.js} +1 -1
- package/dist/web/assets/{system-monitor-body-IZuLn7Qh.js → system-monitor-body-CXiIrn0N.js} +2 -2
- package/dist/web/assets/system-monitor-tab-BJKrTSl0.js +1 -0
- package/dist/web/assets/{system-monitor-window-content-CEaFISgj.js → system-monitor-window-content-Dy9fZJE3.js} +1 -1
- package/dist/web/assets/{tab-host-window-content-D0TN8Wrq.js → tab-host-window-content-BezqrAwC.js} +1 -1
- package/dist/web/assets/{tab-store-OJa1GPCq.js → tab-store-krAuHQ6_.js} +1 -1
- package/dist/web/assets/{team-member-sheet-DDPNcCjd.js → team-member-sheet-CVM2QC3H.js} +2 -2
- package/dist/web/assets/{team-member-window-content-DCiW_1-E.js → team-member-window-content-D4RYKplm.js} +1 -1
- package/dist/web/assets/{terminal-tab-4ypP0NDY.js → terminal-tab-jikhQLct.js} +3 -3
- package/dist/web/assets/{tool-cards-Dd6E7Q3k.js → tool-cards-D5jCQvqn.js} +2 -2
- package/dist/web/assets/{use-blob-url-CPtCcY3i.js → use-blob-url-DMGlPCQD.js} +1 -1
- package/dist/web/assets/{use-inline-blame-vrfFHuaR.js → use-inline-blame-BF8Y1KfV.js} +3 -3
- package/dist/web/assets/{use-lsp-CsuZu49X.js → use-lsp-Bx4oVtlk.js} +1 -1
- package/dist/web/assets/{use-monaco-theme-BIVYJBIn.js → use-monaco-theme-DHczXLMb.js} +1 -1
- package/dist/web/assets/{use-websocket-BMbufhX7.js → use-websocket-BWZkbleJ.js} +1 -1
- package/dist/web/assets/{video-preview-Byj8P_jA.js → video-preview-D191TvX9.js} +1 -1
- package/dist/web/assets/{window-pip-registry-yD6esSju.js → window-pip-registry-D1oXq-Dq.js} +1 -1
- package/dist/web/assets/ws-auth-BG-cdrQ8.js +1 -0
- package/dist/web/index.html +13 -13
- package/dist/web/sw.js +1 -1
- package/docs/architecture/ai-chat-and-providers.md +9 -0
- package/docs/architecture/workspace-and-ui.md +13 -0
- package/package.json +1 -1
- package/packages/ext-git-graph/src/webview-html.test.ts +30 -5
- package/packages/ext-git-graph/src/webview-html.ts +99 -31
- package/src/cli/commands/init.ts +1 -0
- package/src/providers/codex-app-server/codex-history.ts +26 -7
- package/src/providers/codex-app-server/codex-provider.ts +6 -4
- package/src/server/routes/chat.ts +1 -1
- package/src/server/routes/settings.ts +11 -5
- package/src/services/config.service.ts +2 -0
- package/src/services/edge-forwarder.ts +191 -191
- package/src/services/windows-process-tree.ts +351 -351
- package/src/shared/codex-daily-guard.ts +12 -5
- package/src/types/chat.ts +1 -1
- package/src/types/config.ts +12 -1
- package/src/web/components/chat/chat-tab.tsx +162 -38
- package/src/web/components/chat/message-input.tsx +24 -0
- package/src/web/components/chat/model-thinking-selector.tsx +25 -5
- package/src/web/components/chat/new-chat-provider-gate.tsx +68 -0
- package/src/web/components/extensions/extension-tree-view.tsx +4 -11
- package/src/web/components/layout/command-palette.tsx +2 -18
- package/src/web/components/layout/editor-panel.tsx +1 -1
- package/src/web/components/layout/reparenting-tab.tsx +4 -2
- package/src/web/components/layout/tab-bar.tsx +1 -1
- package/src/web/components/settings/accounts/account-card.tsx +2 -2
- package/src/web/components/settings/accounts/codex-accounts-section.tsx +4 -4
- package/src/web/components/settings/ai-settings-section.tsx +36 -0
- package/src/web/components/settings/extension-manager-section.tsx +8 -1
- package/src/web/hooks/use-draft.ts +25 -4
- package/src/web/hooks/use-usage.ts +6 -4
- package/src/web/lib/api-client.ts +7 -1
- package/src/web/lib/api-settings.ts +1 -0
- package/src/web/lib/extension-icons.ts +70 -0
- package/src/web/lib/new-chat-provider.ts +8 -0
- package/src/web/lib/tab-type-icons.ts +14 -0
- package/src/web/stores/panel-store.ts +46 -3
- package/dist/web/assets/accounts-settings-section-BVr9-FZJ.js +0 -1
- package/dist/web/assets/ai-settings-section-B1hQyjAd.js +0 -1
- package/dist/web/assets/ai-settings-section-X2UOfSaS.js +0 -1
- package/dist/web/assets/api-client-pQL53cDQ.js +0 -1
- package/dist/web/assets/chat-tab-DQJfjm8E.js +0 -15
- package/dist/web/assets/extension-manager-section-DG1guILi.js +0 -1
- package/dist/web/assets/index-DU68xk06.js +0 -52
- package/dist/web/assets/keybindings-store-DCRsnKg5.js +0 -1
- package/dist/web/assets/markdown-renderer-Bcaf9_bA.js +0 -1
- package/dist/web/assets/markdown-renderer-DGJp8HC3.js +0 -3
- package/dist/web/assets/notification-store-BXt54VIn.js +0 -1
- package/dist/web/assets/panel-store-CboGJAwh.js +0 -1
- package/dist/web/assets/system-monitor-tab-DiSA490Z.js +0 -1
- package/dist/web/assets/ws-auth-CEYVQ8NH.js +0 -1
- package/packages/vscode-compat/node_modules/.bin/tsc.bunx +0 -0
- package/packages/vscode-compat/node_modules/.bin/tsc.exe +0 -0
- package/packages/vscode-compat/node_modules/.bin/tsserver.bunx +0 -0
- package/packages/vscode-compat/node_modules/.bin/tsserver.exe +0 -0
- package/src/web/hooks/use-delayed-flag.ts +0 -35
|
@@ -3,19 +3,15 @@ import {
|
|
|
3
3
|
Terminal,
|
|
4
4
|
MessageSquare,
|
|
5
5
|
GitCommitHorizontal,
|
|
6
|
-
GitBranch,
|
|
7
6
|
Puzzle,
|
|
8
7
|
Settings,
|
|
9
8
|
Database,
|
|
10
9
|
Search,
|
|
11
|
-
FileCode,
|
|
12
10
|
FilePlus,
|
|
13
11
|
FolderOpen,
|
|
14
12
|
Loader2,
|
|
15
13
|
Globe,
|
|
16
14
|
Mic,
|
|
17
|
-
RefreshCw,
|
|
18
|
-
Plus,
|
|
19
15
|
Columns2,
|
|
20
16
|
Cloud,
|
|
21
17
|
AppWindow,
|
|
@@ -33,6 +29,7 @@ import { useIsTouchOnly } from "@/hooks/use-is-touch-only";
|
|
|
33
29
|
import { useKeybindingsStore } from "@/stores/keybindings-store";
|
|
34
30
|
import { useFileStore, type FileNode } from "@/stores/file-store";
|
|
35
31
|
import { useExtensionStore } from "@/stores/extension-store";
|
|
32
|
+
import { extensionIcon } from "@/lib/extension-icons";
|
|
36
33
|
import { useCompareStore } from "@/stores/compare-store";
|
|
37
34
|
import { usePanelStore } from "@/stores/panel-store";
|
|
38
35
|
import { api } from "@/lib/api-client";
|
|
@@ -61,19 +58,6 @@ interface CommandItem {
|
|
|
61
58
|
|
|
62
59
|
const isMac = typeof navigator !== "undefined" && /Mac|iPhone|iPad/.test(navigator.userAgent);
|
|
63
60
|
|
|
64
|
-
/** Map extension icon string names to lucide components */
|
|
65
|
-
const EXT_ICON_MAP: Record<string, React.ElementType> = {
|
|
66
|
-
"git-branch": GitBranch,
|
|
67
|
-
"database": Database,
|
|
68
|
-
"refresh": RefreshCw,
|
|
69
|
-
"plus": Plus,
|
|
70
|
-
"terminal": Terminal,
|
|
71
|
-
"settings": Settings,
|
|
72
|
-
"search": Search,
|
|
73
|
-
"file-code": FileCode,
|
|
74
|
-
"globe": Globe,
|
|
75
|
-
};
|
|
76
|
-
|
|
77
61
|
/** Format a keybinding combo for display (e.g. "Mod+G" → "⌘G" on Mac, "Ctrl+G" on others) */
|
|
78
62
|
function formatShortcut(combo: string): string {
|
|
79
63
|
if (!combo) return "";
|
|
@@ -296,7 +280,7 @@ export function CommandPalette({ open, onClose, initialQuery = "" }: { open: boo
|
|
|
296
280
|
id: `ext:${cmd.command}`,
|
|
297
281
|
label: cmd.title,
|
|
298
282
|
hint: cmd.category,
|
|
299
|
-
icon: (cmd.icon
|
|
283
|
+
icon: extensionIcon(cmd.icon) ?? Puzzle,
|
|
300
284
|
group: "action" as const,
|
|
301
285
|
keywords: `extension ${cmd.command} ${cmd.category ?? ""}`,
|
|
302
286
|
shortcut: shortcutCombo ? formatShortcut(shortcutCombo) : undefined,
|
|
@@ -54,7 +54,7 @@ export function EditorPanel({ panelId, projectName }: EditorPanelProps) {
|
|
|
54
54
|
panelCount > 1 && "border border-transparent",
|
|
55
55
|
panelCount > 1 && isFocused && "border-primary/30",
|
|
56
56
|
)}
|
|
57
|
-
onMouseDown={() => { if (usePanelStore.getState().focusedPanelId !== panelId) usePanelStore.getState().setFocusedPanel(panelId); }}
|
|
57
|
+
onMouseDown={() => { if (usePanelStore.getState().focusedPanelId !== panelId) usePanelStore.getState().setFocusedPanel(panelId, false); }}
|
|
58
58
|
>
|
|
59
59
|
<TabBar panelId={panelId} />
|
|
60
60
|
|
|
@@ -65,9 +65,11 @@ export function ReparentingTab({
|
|
|
65
65
|
if (pid === DOCK_PANEL_ID || isWindowPanelId(pid)) return;
|
|
66
66
|
usePanelStore.getState().setFocusedPanel(pid);
|
|
67
67
|
};
|
|
68
|
-
wrapper.addEventListener("
|
|
68
|
+
wrapper.addEventListener("pointerdown", onMouseDown, true);
|
|
69
|
+
wrapper.addEventListener("focusin", onMouseDown, true);
|
|
69
70
|
return () => {
|
|
70
|
-
wrapper.removeEventListener("
|
|
71
|
+
wrapper.removeEventListener("pointerdown", onMouseDown, true);
|
|
72
|
+
wrapper.removeEventListener("focusin", onMouseDown, true);
|
|
71
73
|
wrapper.remove();
|
|
72
74
|
};
|
|
73
75
|
}, [wrapper, hiddenContainer]);
|
|
@@ -448,6 +448,7 @@ export const TabBar = memo(function TabBar({ panelId }: TabBarProps) {
|
|
|
448
448
|
metadata: project ? { projectName: project.name } : undefined,
|
|
449
449
|
});
|
|
450
450
|
} else {
|
|
451
|
+
usePanelStore.getState().setFocusedPanel(effectivePanelId, false);
|
|
451
452
|
openCommandPalette();
|
|
452
453
|
}
|
|
453
454
|
}}
|
|
@@ -503,4 +504,3 @@ export const TabBar = memo(function TabBar({ panelId }: TabBarProps) {
|
|
|
503
504
|
</>
|
|
504
505
|
);
|
|
505
506
|
});
|
|
506
|
-
|
|
@@ -278,14 +278,14 @@ export function AccountCard({
|
|
|
278
278
|
</button>
|
|
279
279
|
</TooltipTrigger>
|
|
280
280
|
<TooltipContent side="top" className="max-w-64 text-xs leading-relaxed">
|
|
281
|
-
|
|
281
|
+
Spreads the weekly quota across five weekdays, adding 20% per weekday. Weekend slots keep the previous cap; unused allowance carries forward. Daily slots start at the reset time and use UTC weekdays.
|
|
282
282
|
</TooltipContent>
|
|
283
283
|
</Tooltip>
|
|
284
284
|
</div>
|
|
285
285
|
<p className={`text-[10px] tabular-nums ${dailyGuard.enabled && dailyGuard.state.blocked ? "text-error" : "text-text-subtle"}`}>
|
|
286
286
|
{dailyGuard.enabled && dailyGuard.state.blocked
|
|
287
287
|
? `${Math.round(dailyGuard.state.used * 100)}% used / ${Math.round(dailyGuard.state.cap * 100)}% daily cap. New turns paused.`
|
|
288
|
-
: `
|
|
288
|
+
: `Weekday ${dailyGuard.state.day}/5, ${Math.round(dailyGuard.state.cap * 100)}% daily cap`}
|
|
289
289
|
</p>
|
|
290
290
|
</div>
|
|
291
291
|
{onDailyGuardToggle && (
|
|
@@ -138,18 +138,18 @@ export function CodexAccountsSection() {
|
|
|
138
138
|
</button>
|
|
139
139
|
</TooltipTrigger>
|
|
140
140
|
<TooltipContent side="top" className="max-w-64 text-xs leading-relaxed">
|
|
141
|
-
|
|
141
|
+
Spreads the weekly quota across five weekdays, adding 20% per weekday. Weekend slots keep the previous cap; unused allowance carries forward. Daily slots start at the reset time and use UTC weekdays. Turn it off any time to use the remaining quota freely.
|
|
142
142
|
</TooltipContent>
|
|
143
143
|
</Tooltip>
|
|
144
144
|
</div>
|
|
145
145
|
<p className={`text-[10px] tabular-nums ${a.dailyGuardEnabled && guard.blocked ? "text-error" : "text-text-subtle"}`}>
|
|
146
146
|
{a.dailyGuardEnabled && guard.blocked
|
|
147
147
|
? `${Math.round(guard.used * 100)}% used / ${Math.round(guard.cap * 100)}% daily cap. New turns paused.`
|
|
148
|
-
: `
|
|
148
|
+
: `Weekday ${guard.day}/5, ${Math.round(guard.cap * 100)}% daily cap`}
|
|
149
149
|
</p>
|
|
150
150
|
</div>
|
|
151
|
-
<span className="hidden" title={`
|
|
152
|
-
Daily guard {a.dailyGuardEnabled ? `·
|
|
151
|
+
<span className="hidden" title={`Weekday ${guard.day}/5: hold weekly usage at or below ${Math.round(guard.cap * 100)}% to last until reset.`}>
|
|
152
|
+
Daily guard {a.dailyGuardEnabled ? `· Weekday ${guard.day}/5 cap ${Math.round(guard.cap * 100)}%` : ""}
|
|
153
153
|
</span>
|
|
154
154
|
<Switch
|
|
155
155
|
checked={!!a.dailyGuardEnabled}
|
|
@@ -120,6 +120,42 @@ export function AISettingsSection({ compact }: { compact?: boolean } = {}) {
|
|
|
120
120
|
<div className={gapSize}>
|
|
121
121
|
<h3 className={`${headingSize} font-medium text-text-secondary`}>AI Settings</h3>
|
|
122
122
|
|
|
123
|
+
<div className={fieldGap}>
|
|
124
|
+
<Label htmlFor="ai-new-chat-provider" className={compact ? labelSize : undefined}>New chat provider</Label>
|
|
125
|
+
<Select
|
|
126
|
+
value={settings.new_chat_provider_mode ?? "default"}
|
|
127
|
+
disabled={saving}
|
|
128
|
+
onValueChange={(value) => handleSettingsSave({ new_chat_provider_mode: value as "default" | "follow-focus" })}
|
|
129
|
+
>
|
|
130
|
+
<SelectTrigger id="ai-new-chat-provider" className={`w-full ${compact ? "h-7 text-[11px]" : ""}`}><SelectValue /></SelectTrigger>
|
|
131
|
+
<SelectContent>
|
|
132
|
+
<SelectItem value="follow-focus">Follow last focused chat</SelectItem>
|
|
133
|
+
<SelectItem value="default">Always use default provider</SelectItem>
|
|
134
|
+
</SelectContent>
|
|
135
|
+
</Select>
|
|
136
|
+
<p className={`${compact ? "text-[9px]" : "text-[11px]"} text-muted-foreground`}>
|
|
137
|
+
{settings.new_chat_provider_mode === "follow-focus"
|
|
138
|
+
? "New tabs follow the last chat you focused in this project, across panels. Existing tabs keep their provider."
|
|
139
|
+
: "New tabs always use the default provider. Existing tabs keep their provider."}
|
|
140
|
+
</p>
|
|
141
|
+
</div>
|
|
142
|
+
|
|
143
|
+
<div className={fieldGap}>
|
|
144
|
+
<Label htmlFor="ai-default-provider" className={compact ? labelSize : undefined}>Default provider</Label>
|
|
145
|
+
<Select value={settings.default_provider} disabled={saving}
|
|
146
|
+
onValueChange={(default_provider) => handleSettingsSave({ default_provider })}>
|
|
147
|
+
<SelectTrigger id="ai-default-provider" className={`w-full ${compact ? "h-7 text-[11px]" : ""}`}><SelectValue /></SelectTrigger>
|
|
148
|
+
<SelectContent>
|
|
149
|
+
{providerTabs.map((p) => <SelectItem key={p.id} value={p.id}>{p.name}</SelectItem>)}
|
|
150
|
+
</SelectContent>
|
|
151
|
+
</Select>
|
|
152
|
+
{settings.new_chat_provider_mode === "follow-focus" && (
|
|
153
|
+
<p className={`${compact ? "text-[9px]" : "text-[11px]"} text-muted-foreground`}>
|
|
154
|
+
Used when no chat has been focused in this project yet.
|
|
155
|
+
</p>
|
|
156
|
+
)}
|
|
157
|
+
</div>
|
|
158
|
+
|
|
123
159
|
<div className="flex items-start justify-between gap-3 rounded-md border border-border p-3">
|
|
124
160
|
<div className={fieldGap}>
|
|
125
161
|
<Label htmlFor="ai-share-provider-context" className={compact ? labelSize : undefined}>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useState, useEffect, useCallback } from "react";
|
|
2
2
|
import { Plus, Trash2, Power, PowerOff, Puzzle, FolderSymlink, Loader2 } from "@/lib/icons";
|
|
3
|
+
import { extensionIcon } from "@/lib/extension-icons";
|
|
3
4
|
import { Button } from "@/components/ui/button";
|
|
4
5
|
import { Input } from "@/components/ui/input";
|
|
5
6
|
import { api } from "@/lib/api-client";
|
|
@@ -165,7 +166,13 @@ export function ExtensionManagerSection() {
|
|
|
165
166
|
className="flex items-center gap-2 px-2.5 py-2 rounded-lg bg-muted/50 hover:bg-muted transition-colors"
|
|
166
167
|
>
|
|
167
168
|
<div className="size-8 rounded-md bg-background flex items-center justify-center shrink-0">
|
|
168
|
-
|
|
169
|
+
{/* The manifest's own icon, the same one its panels' tabs get.
|
|
170
|
+
A list of puzzle pieces distinguishes nothing, and the
|
|
171
|
+
field has been on the API all along. */}
|
|
172
|
+
{(() => {
|
|
173
|
+
const Icon = extensionIcon(ext.icon) ?? Puzzle;
|
|
174
|
+
return <Icon className="size-4 text-muted-foreground" />;
|
|
175
|
+
})()}
|
|
169
176
|
</div>
|
|
170
177
|
<div className="flex-1 min-w-0">
|
|
171
178
|
<p className="text-xs font-medium truncate">{ext.displayName || ext.id}</p>
|
|
@@ -86,11 +86,32 @@ export function useDraft(projectName: string, sessionId: string | null) {
|
|
|
86
86
|
[projectName],
|
|
87
87
|
);
|
|
88
88
|
|
|
89
|
-
|
|
90
|
-
|
|
89
|
+
/**
|
|
90
|
+
* Drop a save that is still waiting on the debounce, without touching the server.
|
|
91
|
+
*
|
|
92
|
+
* Called at Enter. The save runs against whatever session the tab is on when the
|
|
93
|
+
* timer fires — and the first send of a new tab swaps the tab onto its new session
|
|
94
|
+
* within that second, so a save left armed would write the message just sent as the
|
|
95
|
+
* new session's draft, to reappear in the composer on the next mount as if unsent.
|
|
96
|
+
*/
|
|
97
|
+
const cancelPendingSave = useCallback(() => {
|
|
98
|
+
if (timerRef.current) clearTimeout(timerRef.current);
|
|
99
|
+
timerRef.current = undefined;
|
|
100
|
+
}, []);
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Clear the draft once its message has actually been handed to the socket.
|
|
104
|
+
*
|
|
105
|
+
* `draftId` names the draft the message was composed under. The first send of a
|
|
106
|
+
* new tab creates the session and only then sends, so by the time the send
|
|
107
|
+
* happens `sessionRef` already points at the new session while the draft still
|
|
108
|
+
* lives under `__new__` — deleting by the current session id would leave it
|
|
109
|
+
* behind and restore it into the next new tab.
|
|
110
|
+
*/
|
|
111
|
+
const clear = useCallback((draftId?: string) => {
|
|
91
112
|
if (!projectName) return;
|
|
92
113
|
if (timerRef.current) clearTimeout(timerRef.current);
|
|
93
|
-
const id = sessionRef.current ?? "__new__";
|
|
114
|
+
const id = draftId ?? sessionRef.current ?? "__new__";
|
|
94
115
|
api
|
|
95
116
|
.del(`${projectUrl(projectName)}/chat/drafts/${encodeURIComponent(id)}`)
|
|
96
117
|
.catch(() => {});
|
|
@@ -104,5 +125,5 @@ export function useDraft(projectName: string, sessionId: string | null) {
|
|
|
104
125
|
};
|
|
105
126
|
}, []);
|
|
106
127
|
|
|
107
|
-
return { draft, draftLoading: loading, saveDraft: save, clearDraft: clear };
|
|
128
|
+
return { draft, draftLoading: loading, saveDraft: save, clearDraft: clear, cancelPendingSave };
|
|
108
129
|
}
|
|
@@ -26,8 +26,9 @@ interface UseUsageReturn {
|
|
|
26
26
|
* `sessionId` scopes the reported account to this session's binding. Without it the header
|
|
27
27
|
* shows whichever account ran last across every open session, which is wrong for all but one.
|
|
28
28
|
*/
|
|
29
|
-
export function useUsage(projectName: string, providerId = "claude", sessionId?: string): UseUsageReturn {
|
|
30
|
-
const
|
|
29
|
+
export function useUsage(projectName: string, providerId = "claude", sessionId?: string, pickedAccountId?: string): UseUsageReturn {
|
|
30
|
+
const previewAccountId = providerId === "codex" && !sessionId ? pickedAccountId : undefined;
|
|
31
|
+
const scope = JSON.stringify([projectName, providerId, sessionId, previewAccountId]);
|
|
31
32
|
const [snapshot, setSnapshot] = useState<{ scope: string; usage: UsageInfo; fetchedAt: string | null } | null>(null);
|
|
32
33
|
const [usageLoading, setUsageLoading] = useState(false);
|
|
33
34
|
const timerRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
|
@@ -39,11 +40,12 @@ export function useUsage(projectName: string, providerId = "claude", sessionId?:
|
|
|
39
40
|
setUsageLoading(true);
|
|
40
41
|
const qs = forceRefresh ? "&refresh=1" : "";
|
|
41
42
|
const sessionQs = sessionId ? `&session=${encodeURIComponent(sessionId)}` : "";
|
|
43
|
+
const accountQs = previewAccountId ? `&accountId=${encodeURIComponent(previewAccountId)}` : "";
|
|
42
44
|
// Via api.get, not raw fetch: the toolbar's loading state is gated on this
|
|
43
45
|
// settling, and a raw fetch has no timeout to stop it stalling forever.
|
|
44
46
|
return api
|
|
45
47
|
.get<(UsageInfo & { lastFetchedAt?: string }) | null>(
|
|
46
|
-
`${projectUrl(projectName)}/chat/usage?providerId=${providerId}${sessionQs}${qs}`,
|
|
48
|
+
`${projectUrl(projectName)}/chat/usage?providerId=${providerId}${sessionQs}${accountQs}${qs}`,
|
|
47
49
|
)
|
|
48
50
|
.then((data) => {
|
|
49
51
|
if (request !== requestRef.current) return;
|
|
@@ -55,7 +57,7 @@ export function useUsage(projectName: string, providerId = "claude", sessionId?:
|
|
|
55
57
|
.finally(() => {
|
|
56
58
|
if (request === requestRef.current) setUsageLoading(false);
|
|
57
59
|
});
|
|
58
|
-
}, [projectName, providerId, sessionId, scope]);
|
|
60
|
+
}, [projectName, providerId, sessionId, previewAccountId, scope]);
|
|
59
61
|
|
|
60
62
|
// Read cache on mount + auto-read every POLL_INTERVAL
|
|
61
63
|
useEffect(() => {
|
|
@@ -122,11 +122,17 @@ export class ApiClient {
|
|
|
122
122
|
}
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
-
|
|
125
|
+
/**
|
|
126
|
+
* No default timeout, unlike `get`: a POST is not idempotent, so cutting one off
|
|
127
|
+
* mid-flight can leave the server having done the work with nobody to tell. A
|
|
128
|
+
* caller that would rather fail loudly than wait passes its own `signal`.
|
|
129
|
+
*/
|
|
130
|
+
async post<T>(path: string, body?: unknown, options?: { signal?: AbortSignal }): Promise<T> {
|
|
126
131
|
const res = await fetch(`${this.baseUrl}${path}`, {
|
|
127
132
|
method: "POST",
|
|
128
133
|
headers: this.headers(),
|
|
129
134
|
body: body != null ? JSON.stringify(body) : undefined,
|
|
135
|
+
signal: options?.signal,
|
|
130
136
|
});
|
|
131
137
|
return this.handleResponse<T>(res);
|
|
132
138
|
}
|
|
@@ -212,6 +212,7 @@ export interface AIProviderSettings {
|
|
|
212
212
|
|
|
213
213
|
export interface AISettings {
|
|
214
214
|
default_provider: string;
|
|
215
|
+
new_chat_provider_mode?: "default" | "follow-focus";
|
|
215
216
|
share_provider_context?: boolean;
|
|
216
217
|
providers: Record<string, AIProviderSettings>;
|
|
217
218
|
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The icons an extension may ask for by name, and the glyph each name draws.
|
|
3
|
+
*
|
|
4
|
+
* A manifest names an icon the way VS Code's does — `{ "command":
|
|
5
|
+
* "git-graph.view", "icon": "git-branch" }` — and without a table like this one
|
|
6
|
+
* the name is inert: every panel an extension opens falls back to the generic
|
|
7
|
+
* puzzle piece, so a strip holding the graph, a blame, a file history and a
|
|
8
|
+
* rebase shows four identical tabs.
|
|
9
|
+
*
|
|
10
|
+
* Kebab-case, the way a manifest writes it. A name with no entry here is not an
|
|
11
|
+
* error — the caller's fallback draws instead — but `tests/unit/web/
|
|
12
|
+
* extension-icons.test.ts` fails when a *bundled* extension asks for one,
|
|
13
|
+
* because that is the case where the puzzle piece is a bug rather than a
|
|
14
|
+
* default.
|
|
15
|
+
*/
|
|
16
|
+
import {
|
|
17
|
+
Database, FileCode, FileDiff, GitBranch, GitCommitHorizontal, Globe, History, ListOrdered, Pencil,
|
|
18
|
+
Plus, RefreshCw, Search, Settings, Terminal, Trash2, Users,
|
|
19
|
+
} from "@/lib/icons";
|
|
20
|
+
import type { ElementType } from "react";
|
|
21
|
+
import type { ExtensionContributes } from "../../types/extension.ts";
|
|
22
|
+
|
|
23
|
+
export const EXTENSION_ICONS: Record<string, ElementType> = {
|
|
24
|
+
// Named by the panels the bundled Git Graph contributes.
|
|
25
|
+
"git-branch": GitBranch,
|
|
26
|
+
"git-commit": GitCommitHorizontal,
|
|
27
|
+
// Fluent draws no compare glyph and PPM re-exports no lucide one, so a
|
|
28
|
+
// comparison of two refs wears the same icon as any other diff in the app.
|
|
29
|
+
"git-compare": FileDiff,
|
|
30
|
+
history: History,
|
|
31
|
+
users: Users,
|
|
32
|
+
"list-ordered": ListOrdered,
|
|
33
|
+
// Named by tree-view item actions.
|
|
34
|
+
refresh: RefreshCw,
|
|
35
|
+
edit: Pencil,
|
|
36
|
+
trash: Trash2,
|
|
37
|
+
plus: Plus,
|
|
38
|
+
search: Search,
|
|
39
|
+
// General-purpose names an extension is likely to reach for.
|
|
40
|
+
database: Database,
|
|
41
|
+
terminal: Terminal,
|
|
42
|
+
settings: Settings,
|
|
43
|
+
"file-code": FileCode,
|
|
44
|
+
globe: Globe,
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/** The glyph for a manifest icon name, or undefined for one nothing draws. */
|
|
48
|
+
export function extensionIcon(name: string | undefined | null): ElementType | undefined {
|
|
49
|
+
return name ? EXTENSION_ICONS[name] : undefined;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* The icon of the command that opens a panel.
|
|
54
|
+
*
|
|
55
|
+
* A panel's viewType *is* its command id — `registerViewCommand` requires the
|
|
56
|
+
* two to match, because the frontend pairs them by slug — so the manifest entry
|
|
57
|
+
* that names the command already names the panel, and an extension needs no
|
|
58
|
+
* second place to declare one. The tab carries the slug (a trailing `.view`
|
|
59
|
+
* stripped), so both forms are compared.
|
|
60
|
+
*/
|
|
61
|
+
export function viewTypeIcon(
|
|
62
|
+
contributions: ExtensionContributes | null | undefined,
|
|
63
|
+
viewType: string | undefined,
|
|
64
|
+
): ElementType | undefined {
|
|
65
|
+
if (!contributions?.commands || !viewType) return undefined;
|
|
66
|
+
const command = contributions.commands.find(
|
|
67
|
+
(c) => c.command === viewType || c.command.replace(/\.view$/, "") === viewType,
|
|
68
|
+
);
|
|
69
|
+
return extensionIcon(command?.icon);
|
|
70
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { AISettings } from "./api-settings";
|
|
2
|
+
|
|
3
|
+
/** The source is captured when the tab opens, before another panel can gain focus. */
|
|
4
|
+
export function resolveNewChatProvider(settings: AISettings, focusedProvider?: string): string {
|
|
5
|
+
return settings.new_chat_provider_mode === "follow-focus" && focusedProvider
|
|
6
|
+
? focusedProvider
|
|
7
|
+
: settings.default_provider || "claude";
|
|
8
|
+
}
|
|
@@ -14,6 +14,8 @@ import type { ElementType } from "react";
|
|
|
14
14
|
import type { TabType } from "@/stores/tab-store";
|
|
15
15
|
import { fileIconElement } from "@/lib/file-icons";
|
|
16
16
|
import { PROVIDER_LOGOS } from "@/lib/provider-logos";
|
|
17
|
+
import { viewTypeIcon } from "@/lib/extension-icons";
|
|
18
|
+
import { useExtensionStore } from "@/stores/extension-store";
|
|
17
19
|
|
|
18
20
|
export const TAB_TYPE_ICONS: Record<TabType, LucideIcon> = {
|
|
19
21
|
terminal: Terminal,
|
|
@@ -70,5 +72,17 @@ export function getTabIcon(tab: TabIconSubject): ElementType {
|
|
|
70
72
|
const logo = PROVIDER_LOGOS[(tab.metadata?.providerId as string | undefined) ?? "claude"];
|
|
71
73
|
if (logo) return logo;
|
|
72
74
|
}
|
|
75
|
+
// A panel an extension opened is labelled with the icon its manifest gave the
|
|
76
|
+
// command that opens it — read here rather than stored on the tab, so a tab
|
|
77
|
+
// that was already open when the extension declared one picks it up too.
|
|
78
|
+
// Falls back to the puzzle piece, which is the honest answer for a panel
|
|
79
|
+
// whose extension has not said what it is.
|
|
80
|
+
if (tab.type === "extension" || tab.type === "extension-webview") {
|
|
81
|
+
const contributed = viewTypeIcon(
|
|
82
|
+
useExtensionStore.getState().contributions,
|
|
83
|
+
tab.metadata?.viewType as string | undefined,
|
|
84
|
+
);
|
|
85
|
+
if (contributed) return contributed;
|
|
86
|
+
}
|
|
73
87
|
return getTabTypeIcon(tab.type);
|
|
74
88
|
}
|
|
@@ -89,6 +89,8 @@ export interface PanelStore {
|
|
|
89
89
|
grid: string[][];
|
|
90
90
|
focusedPanelId: string;
|
|
91
91
|
currentProject: string | null;
|
|
92
|
+
/** Session-local focus memory, isolated by project; non-chat tabs do not clear it. */
|
|
93
|
+
lastFocusedChatProviders: Record<string, string>;
|
|
92
94
|
|
|
93
95
|
/** Keep-alive: per-project grid snapshots (for hidden workspaces) */
|
|
94
96
|
projectGrids: Record<string, string[][]>;
|
|
@@ -106,7 +108,7 @@ export interface PanelStore {
|
|
|
106
108
|
reloadProject: (projectName: string) => void;
|
|
107
109
|
|
|
108
110
|
// Panel focus
|
|
109
|
-
setFocusedPanel: (panelId: string) => void;
|
|
111
|
+
setFocusedPanel: (panelId: string, rememberChat?: boolean) => void;
|
|
110
112
|
|
|
111
113
|
// Tab operations (operate on focused panel by default)
|
|
112
114
|
openTab: (tab: Omit<Tab, "id">, panelId?: string) => string;
|
|
@@ -210,6 +212,7 @@ export const usePanelStore = create<PanelStore>()((set, get) => {
|
|
|
210
212
|
return {
|
|
211
213
|
...defaultLayout(),
|
|
212
214
|
currentProject: null,
|
|
215
|
+
lastFocusedChatProviders: {},
|
|
213
216
|
projectGrids: {},
|
|
214
217
|
projectFocused: {},
|
|
215
218
|
dock: { visible: false, height: 30 },
|
|
@@ -459,10 +462,13 @@ export const usePanelStore = create<PanelStore>()((set, get) => {
|
|
|
459
462
|
get().switchProject(projectName);
|
|
460
463
|
},
|
|
461
464
|
|
|
462
|
-
setFocusedPanel: (panelId) => {
|
|
465
|
+
setFocusedPanel: (panelId, rememberChat = true) => {
|
|
463
466
|
// Focus drives where the next tab opens, so it must stay on a real, on-grid panel.
|
|
464
467
|
if (isWindowPanelId(panelId)) return;
|
|
465
|
-
|
|
468
|
+
const panel = get().panels[panelId];
|
|
469
|
+
if (!panel) return;
|
|
470
|
+
set({ focusedPanelId: panelId });
|
|
471
|
+
if (rememberChat) rememberChatProvider(panel.tabs.find((t) => t.id === panel.activeTabId));
|
|
466
472
|
},
|
|
467
473
|
|
|
468
474
|
openTab: (tabDef, panelId?) => {
|
|
@@ -474,6 +480,15 @@ export const usePanelStore = create<PanelStore>()((set, get) => {
|
|
|
474
480
|
const panel = get().panels[pid];
|
|
475
481
|
if (!panel) return "";
|
|
476
482
|
|
|
483
|
+
if (tabDef.type === "chat" && !tabDef.metadata?.sessionId && !tabDef.metadata?.providerId) {
|
|
484
|
+
const project = tabDef.projectId ?? (tabDef.metadata?.projectName as string | undefined) ?? get().currentProject;
|
|
485
|
+
tabDef = { ...tabDef, metadata: {
|
|
486
|
+
...tabDef.metadata,
|
|
487
|
+
providerPending: true,
|
|
488
|
+
focusedProviderOnOpen: project ? get().lastFocusedChatProviders[project] : undefined,
|
|
489
|
+
} };
|
|
490
|
+
}
|
|
491
|
+
|
|
477
492
|
// Terminal: compute next available index if not provided
|
|
478
493
|
if (tabDef.type === "terminal" && !tabDef.metadata?.terminalIndex) {
|
|
479
494
|
const allTabs = Object.values(get().panels).flatMap((p) => p.tabs);
|
|
@@ -499,6 +514,7 @@ export const usePanelStore = create<PanelStore>()((set, get) => {
|
|
|
499
514
|
for (const p of Object.values(get().panels)) {
|
|
500
515
|
const existing = p.tabs.find((t) => t.type === "chat" && t.metadata?.sessionId === sid);
|
|
501
516
|
if (existing) {
|
|
517
|
+
rememberChatProvider(existing);
|
|
502
518
|
set((s) => ({
|
|
503
519
|
focusedPanelId: focusAfterActivate(p.id),
|
|
504
520
|
panels: {
|
|
@@ -664,6 +680,7 @@ export const usePanelStore = create<PanelStore>()((set, get) => {
|
|
|
664
680
|
setActiveTab: (tabId, panelId?) => {
|
|
665
681
|
const panel = panelId ? get().panels[panelId] : findPanel(tabId);
|
|
666
682
|
if (!panel) return;
|
|
683
|
+
rememberChatProvider(panel.tabs.find((t) => t.id === tabId));
|
|
667
684
|
const pid = panel.id;
|
|
668
685
|
set((s) => {
|
|
669
686
|
const p = s.panels[pid]!;
|
|
@@ -862,3 +879,29 @@ export const usePanelStore = create<PanelStore>()((set, get) => {
|
|
|
862
879
|
isMobile: () => typeof window !== "undefined" && window.innerWidth < 768,
|
|
863
880
|
};
|
|
864
881
|
});
|
|
882
|
+
|
|
883
|
+
// Observe activation as well as a provider change in the focused composer. Updates
|
|
884
|
+
// from background chats must never replace the user's most recent focus choice.
|
|
885
|
+
function rememberChatProvider(tab: Tab | undefined) {
|
|
886
|
+
const state = usePanelStore.getState();
|
|
887
|
+
if (tab?.type !== "chat" || tab.metadata?.providerPending) return;
|
|
888
|
+
const project = tab.projectId ?? (tab.metadata?.projectName as string | undefined) ?? state.currentProject;
|
|
889
|
+
const provider = (tab.metadata?.providerId as string | undefined) ?? "claude";
|
|
890
|
+
if (!project || state.lastFocusedChatProviders[project] === provider) return;
|
|
891
|
+
usePanelStore.setState({ lastFocusedChatProviders: {
|
|
892
|
+
...state.lastFocusedChatProviders, [project]: provider,
|
|
893
|
+
} });
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
usePanelStore.subscribe((state, previous) => {
|
|
897
|
+
const panel = state.panels[state.focusedPanelId];
|
|
898
|
+
const tab = panel?.tabs.find((t) => t.id === panel.activeTabId);
|
|
899
|
+
// A panel's chrome can change the destination for new tabs without focusing
|
|
900
|
+
// its chat. Actual body/tab focus is recorded explicitly by the actions above.
|
|
901
|
+
const oldPanel = previous.panels[state.focusedPanelId];
|
|
902
|
+
const oldTab = oldPanel?.tabs.find((t) => t.id === oldPanel.activeTabId);
|
|
903
|
+
if (tab?.id === oldTab?.id
|
|
904
|
+
&& tab?.metadata?.providerId === oldTab?.metadata?.providerId
|
|
905
|
+
&& state.currentProject === previous.currentProject) return;
|
|
906
|
+
rememberChatProvider(tab);
|
|
907
|
+
});
|