claudedesk 4.5.0 → 4.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/CHANGELOG.md +31 -1
  2. package/CLAUDE.md +8 -3
  3. package/dist/main/index.js +10 -1
  4. package/dist/main/ipc-handlers.js +107 -1
  5. package/dist/main/quota-service.js +6 -4
  6. package/dist/main/tunnel-manager.js +546 -0
  7. package/dist/renderer/assets/{index-DPk5RtXR.js → index-BtxPa5XT.js} +3537 -1765
  8. package/dist/renderer/assets/{index-DTGEErZK.css → index-Df3cuu7-.css} +1 -1
  9. package/dist/renderer/index.html +2 -2
  10. package/dist/shared/ipc-contract.js +36 -0
  11. package/dist/shared/types/tunnel-types.js +2 -0
  12. package/docs/repo-index.md +15 -2
  13. package/docs/tunnel-implementation-plan.md +255 -0
  14. package/docs/tunnel-ui-spec.md +1117 -0
  15. package/package.json +1 -1
  16. package/src/main/index.ts +12 -1
  17. package/src/main/ipc-handlers.ts +68 -1
  18. package/src/main/quota-service.ts +6 -4
  19. package/src/main/tunnel-manager.ts +622 -0
  20. package/src/renderer/App.tsx +19 -0
  21. package/src/renderer/components/TunnelCreateDialog.tsx +669 -0
  22. package/src/renderer/components/TunnelPanel.tsx +2005 -0
  23. package/src/renderer/components/TunnelRequestLogs.tsx +568 -0
  24. package/src/renderer/components/ui/BudgetPanel.tsx +17 -0
  25. package/src/renderer/components/ui/FuelTooltip.tsx +10 -2
  26. package/src/renderer/components/ui/TabBar.tsx +7 -1
  27. package/src/renderer/components/ui/ToolsDropdown.tsx +21 -0
  28. package/src/renderer/hooks/useTunnel.ts +266 -0
  29. package/src/shared/ipc-contract.ts +73 -0
  30. package/src/shared/ipc-types.ts +17 -0
  31. package/src/shared/types/tunnel-types.ts +80 -0
  32. package/PHASE_1_IMPLEMENTATION.md +0 -313
  33. package/PHASE_2_PARTIAL_IMPLEMENTATION.md +0 -286
package/CHANGELOG.md CHANGED
@@ -13,6 +13,35 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
13
13
 
14
14
  ---
15
15
 
16
+ ## [4.6.0] - 2026-02-19
17
+
18
+ ### Added
19
+ - **LaunchTunnel integration** — Expose local ports to the internet via LaunchTunnel (14th domain)
20
+ - `TunnelManager` with hybrid REST API + CLI (`lt preview`) process management
21
+ - API key management with validation, stored in `~/.claudedesk/tunnel-settings.json`
22
+ - Tunnel list with 30s cache, status mapping (API snake_case → camelCase)
23
+ - Account info section (email, plan badge, status)
24
+ - Request log viewer with method/path/status/duration/size columns
25
+ - TunnelPanel (4 views: setup/main/settings/logs), TunnelCreateDialog, TunnelRequestLogs
26
+ - CLI auto-detection via `where`/`which`, `shell: true` for cross-platform spawn
27
+ - Ctrl+Shift+U keyboard shortcut, ToolsDropdown entry with active count badge
28
+ - 17 IPC methods (`tunnel:*`): 13 invoke + 4 events
29
+
30
+ ### Fixed
31
+ - **Tunnel spawn ENOENT** — Use `shell: true` in spawn for cross-platform `.cmd` shim compatibility
32
+ - **Tunnel status mapping** — API returns `"active"` not `"connected"`; added to `mapApiStatus`
33
+ - **Tunnel CLI subcommand** — Fixed `lt preview --port` (was missing `preview` subcommand)
34
+ - **Tunnel URL parsing** — Match `URL:` output format from LaunchTunnel CLI (not `your url is:`)
35
+ - **API snake_case mapping** — Map `created_at`, `status_code`, `duration_ms` etc. to camelCase
36
+ - **Account response unwrapping** — API returns `{"user": {...}}` wrapper; now unwrapped correctly
37
+ - **Request logs endpoint** — Reverted from `/requests` back to correct `/logs` path
38
+
39
+ ### Changed
40
+ - **IPC contract** — Expanded from 149 to ~166 methods (added 17 tunnel methods)
41
+ - **Project scale** — ~150 source files, ~49,000 LOC, 14 domains, 14 managers
42
+
43
+ ---
44
+
16
45
  ## [4.5.0] - 2026-02-17
17
46
 
18
47
  ### Added
@@ -251,7 +280,8 @@ See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines on suggesting changes and
251
280
 
252
281
  ---
253
282
 
254
- [Unreleased]: https://github.com/carloluisito/claudedesk/compare/v4.5.0...HEAD
283
+ [Unreleased]: https://github.com/carloluisito/claudedesk/compare/v4.6.0...HEAD
284
+ [4.6.0]: https://github.com/carloluisito/claudedesk/compare/v4.5.0...v4.6.0
255
285
  [4.5.0]: https://github.com/carloluisito/claudedesk/compare/v4.4.1...v4.5.0
256
286
  [4.4.1]: https://github.com/carloluisito/claudedesk/compare/v4.3.1...v4.4.1
257
287
  [4.3.1]: https://github.com/carloluisito/claudedesk/compare/v4.3.0...v4.3.1
package/CLAUDE.md CHANGED
@@ -2,6 +2,10 @@
2
2
 
3
3
  Electron desktop app wrapping Claude Code CLI with multi-session terminals, split views, and agent team visualization.
4
4
 
5
+ ## Workflow Rule
6
+
7
+ **Always run the `requirements-clarifier` agent (via the Task tool) on the user's initial prompt before planning or implementing.** This ensures requirements are analyzed, ambiguities are surfaced, and acceptance criteria are established before any work begins. Skip only for trivial tasks (typo fixes, single-line changes, or purely informational questions).
8
+
5
9
  ## Tech Stack
6
10
 
7
11
  Electron 28 | React 18 | TypeScript | xterm.js | node-pty | Tailwind CSS | reactflow
@@ -11,9 +15,9 @@ Electron 28 | React 18 | TypeScript | xterm.js | node-pty | Tailwind CSS | react
11
15
  ```
12
16
  ┌─────────────────────────────────────────────┐
13
17
  │ Main Process (Node.js) │
14
- 13 managers + IPC handlers + session pool │
18
+ 14 managers + IPC handlers + session pool │
15
19
  └──────────────────┬──────────────────────────┘
16
- │ IPC (149 methods)
20
+ │ IPC (~166 methods)
17
21
  ┌──────────────────┴──────────────────────────┐
18
22
  │ Preload (auto-derived context bridge) │
19
23
  └──────────────────┬──────────────────────────┘
@@ -26,7 +30,7 @@ Electron 28 | React 18 | TypeScript | xterm.js | node-pty | Tailwind CSS | react
26
30
 
27
31
  **3-layer pattern per domain:** Manager (main) → Hook (renderer) → Components (renderer)
28
32
 
29
- **IPC contract** (`src/shared/ipc-contract.ts`) is the single source of truth — 149 methods. The preload bridge and `ElectronAPI` type are auto-derived from it.
33
+ **IPC contract** (`src/shared/ipc-contract.ts`) is the single source of truth — ~166 methods. The preload bridge and `ElectronAPI` type are auto-derived from it.
30
34
 
31
35
  ## Domain Map
32
36
 
@@ -44,6 +48,7 @@ Electron 28 | React 18 | TypeScript | xterm.js | node-pty | Tailwind CSS | react
44
48
  | Atlas | atlas-manager | useAtlas, AtlasPanel | types/atlas-types.ts | `atlas:*` |
45
49
  | Git | git-manager | useGit, useDiffViewer, GitPanel, DiffViewer, DiffFileNav, DiffViewerHeader, DiffContentArea, CommitDialog, WorktreePanel, WorktreeCleanupDialog, diff-parser | types/git-types.ts | `git:*` |
46
50
  | Playbooks | playbook-manager, playbook-executor, built-in-playbooks | usePlaybooks, PlaybookPicker, PlaybookParameterDialog, PlaybookProgressPanel, PlaybookPanel, PlaybookEditor | types/playbook-types.ts | `playbook:*` |
51
+ | Tunnels | tunnel-manager | useTunnel, TunnelPanel, TunnelCreateDialog, TunnelRequestLogs | types/tunnel-types.ts | `tunnel:*` |
47
52
  | Window | index.ts | ConfirmDialog, SettingsDialog, AboutDialog, TitleBarBranding | ipc-types.ts | `window:*`, `dialog:*` |
48
53
 
49
54
  ## Adding a New IPC Method
@@ -51,6 +51,7 @@ const model_history_manager_1 = require("./model-history-manager");
51
51
  const git_manager_1 = require("./git-manager");
52
52
  const playbook_manager_1 = require("./playbook-manager");
53
53
  const playbook_executor_1 = require("./playbook-executor");
54
+ const tunnel_manager_1 = require("./tunnel-manager");
54
55
  const ipc_handlers_1 = require("./ipc-handlers");
55
56
  // Prevent EPIPE crashes when stdout/stderr pipe breaks (e.g., renderer window closes while PTY is active)
56
57
  process.stdout?.on?.('error', (err) => { if (err.code !== 'EPIPE')
@@ -75,6 +76,7 @@ let modelHistoryManager = null;
75
76
  let gitManager = null;
76
77
  let playbookManager = null;
77
78
  let playbookExecutor = null;
79
+ let tunnelManager = null;
78
80
  const CONFIG_DIR = path.join(electron_1.app.getPath('home'), '.claudedesk');
79
81
  const WINDOW_STATE_FILE = path.join(CONFIG_DIR, 'window-state.json');
80
82
  function ensureConfigDir() {
@@ -187,6 +189,9 @@ function createWindow() {
187
189
  // Initialize playbook manager + executor
188
190
  playbookManager = new playbook_manager_1.PlaybookManager();
189
191
  playbookExecutor = new playbook_executor_1.PlaybookExecutor(sessionManager, checkpointManager, playbookManager);
192
+ // Initialize tunnel manager
193
+ tunnelManager = new tunnel_manager_1.TunnelManager();
194
+ tunnelManager.setMainWindow(mainWindow);
190
195
  // Initialize command registry
191
196
  commandRegistry = new command_registry_1.CommandRegistry();
192
197
  // Initialize agent team manager
@@ -198,7 +203,7 @@ function createWindow() {
198
203
  agentTeamManager?.onSessionClosed(sessionId);
199
204
  });
200
205
  // Setup IPC handlers with pool reference
201
- (0, ipc_handlers_1.setupIPCHandlers)(mainWindow, sessionManager, settingsManager, templatesManager, historyManager, checkpointManager, sessionPool, agentTeamManager, atlasManager, layoutPresetsManager, commandRegistry, modelHistoryManager, gitManager, playbookManager, playbookExecutor);
206
+ (0, ipc_handlers_1.setupIPCHandlers)(mainWindow, sessionManager, settingsManager, templatesManager, historyManager, checkpointManager, sessionPool, agentTeamManager, atlasManager, layoutPresetsManager, commandRegistry, modelHistoryManager, gitManager, playbookManager, playbookExecutor, tunnelManager);
202
207
  // Initialize pool (delayed, async)
203
208
  setTimeout(() => {
204
209
  if (sessionPool) {
@@ -243,6 +248,10 @@ function createWindow() {
243
248
  });
244
249
  mainWindow.on('closed', () => {
245
250
  (0, ipc_handlers_1.removeIPCHandlers)();
251
+ if (tunnelManager) {
252
+ tunnelManager.destroy();
253
+ tunnelManager = null;
254
+ }
246
255
  if (modelHistoryManager) {
247
256
  modelHistoryManager.shutdown();
248
257
  modelHistoryManager = null;
@@ -43,7 +43,7 @@ const file_dragdrop_handler_1 = require("./file-dragdrop-handler");
43
43
  const ipc_emitter_1 = require("./ipc-emitter");
44
44
  const ipc_registry_1 = require("./ipc-registry");
45
45
  let registry = null;
46
- function setupIPCHandlers(mainWindow, sessionManager, settingsManager, templatesManager, historyManager, checkpointManager, sessionPool, agentTeamManager, atlasManager, layoutPresetsManager, commandRegistry, modelHistoryManager, gitManager, playbookManager, playbookExecutor) {
46
+ function setupIPCHandlers(mainWindow, sessionManager, settingsManager, templatesManager, historyManager, checkpointManager, sessionPool, agentTeamManager, atlasManager, layoutPresetsManager, commandRegistry, modelHistoryManager, gitManager, playbookManager, playbookExecutor, tunnelManager) {
47
47
  // Connect managers to window
48
48
  sessionManager.setMainWindow(mainWindow);
49
49
  checkpointManager.setMainWindow(mainWindow);
@@ -1028,6 +1028,112 @@ function setupIPCHandlers(mainWindow, sessionManager, settingsManager, templates
1028
1028
  claudeVersion,
1029
1029
  };
1030
1030
  });
1031
+ // ── LaunchTunnel ──
1032
+ registry.handle('tunnelList', async () => {
1033
+ try {
1034
+ return await tunnelManager.list();
1035
+ }
1036
+ catch (err) {
1037
+ console.error('Failed to list tunnels:', err);
1038
+ throw err;
1039
+ }
1040
+ });
1041
+ registry.handle('tunnelCreate', async (_e, request) => {
1042
+ try {
1043
+ return await tunnelManager.create(request);
1044
+ }
1045
+ catch (err) {
1046
+ console.error('Failed to create tunnel:', err);
1047
+ throw err;
1048
+ }
1049
+ });
1050
+ registry.handle('tunnelStop', async (_e, tunnelId) => {
1051
+ try {
1052
+ return await tunnelManager.stop(tunnelId);
1053
+ }
1054
+ catch (err) {
1055
+ console.error('Failed to stop tunnel:', err);
1056
+ throw err;
1057
+ }
1058
+ });
1059
+ registry.handle('tunnelGetInfo', async (_e, tunnelId) => {
1060
+ try {
1061
+ return await tunnelManager.getInfo(tunnelId);
1062
+ }
1063
+ catch (err) {
1064
+ console.error('Failed to get tunnel info:', err);
1065
+ throw err;
1066
+ }
1067
+ });
1068
+ registry.handle('tunnelGetLogs', async (_e, tunnelId, limit) => {
1069
+ try {
1070
+ return await tunnelManager.getLogs(tunnelId, limit);
1071
+ }
1072
+ catch (err) {
1073
+ console.error('Failed to get tunnel logs:', err);
1074
+ throw err;
1075
+ }
1076
+ });
1077
+ registry.handle('tunnelGetSettings', async () => {
1078
+ return tunnelManager.getSettings();
1079
+ });
1080
+ registry.handle('tunnelUpdateSettings', async (_e, settings) => {
1081
+ try {
1082
+ return tunnelManager.updateSettings(settings);
1083
+ }
1084
+ catch (err) {
1085
+ console.error('Failed to update tunnel settings:', err);
1086
+ throw err;
1087
+ }
1088
+ });
1089
+ registry.handle('tunnelGetAccount', async () => {
1090
+ try {
1091
+ return await tunnelManager.getAccount();
1092
+ }
1093
+ catch (err) {
1094
+ console.error('Failed to get tunnel account:', err);
1095
+ throw err;
1096
+ }
1097
+ });
1098
+ registry.handle('tunnelGetUsage', async (_e, tunnelId) => {
1099
+ try {
1100
+ return await tunnelManager.getUsage(tunnelId);
1101
+ }
1102
+ catch (err) {
1103
+ console.error('Failed to get tunnel usage:', err);
1104
+ throw err;
1105
+ }
1106
+ });
1107
+ registry.handle('tunnelRefresh', async () => {
1108
+ try {
1109
+ return await tunnelManager.refresh();
1110
+ }
1111
+ catch (err) {
1112
+ console.error('Failed to refresh tunnels:', err);
1113
+ throw err;
1114
+ }
1115
+ });
1116
+ registry.handle('tunnelDetectBinary', async () => {
1117
+ return tunnelManager.detectBinary();
1118
+ });
1119
+ registry.handle('tunnelValidateKey', async (_e, apiKey) => {
1120
+ try {
1121
+ return await tunnelManager.validateKey(apiKey);
1122
+ }
1123
+ catch (err) {
1124
+ console.error('Failed to validate tunnel API key:', err);
1125
+ throw err;
1126
+ }
1127
+ });
1128
+ registry.handle('tunnelStopAll', async () => {
1129
+ try {
1130
+ return await tunnelManager.stopAll();
1131
+ }
1132
+ catch (err) {
1133
+ console.error('Failed to stop all tunnels:', err);
1134
+ throw err;
1135
+ }
1136
+ });
1031
1137
  // ── Session I/O (send — fire and forget) ──
1032
1138
  registry.on('sendSessionInput', (_e, input) => {
1033
1139
  sessionManager.sendInput(input.sessionId, input.data);
@@ -275,12 +275,14 @@ function buildBurnRateResult(rate5h, rate7d, current, history) {
275
275
  else if (recentRate < olderRate - 0.5)
276
276
  trend = 'decreasing';
277
277
  }
278
- // Label based on projected time to limit
278
+ // Label based on projected time to limit (worst of both quotas)
279
279
  let label = 'on-track';
280
- if (projectedMinutes5h !== null) {
281
- if (projectedMinutes5h < 60)
280
+ const projections = [projectedMinutes5h, projectedMinutes7d].filter((v) => v !== null);
281
+ if (projections.length > 0) {
282
+ const minProjected = Math.min(...projections);
283
+ if (minProjected < 60)
282
284
  label = 'critical';
283
- else if (projectedMinutes5h < 120)
285
+ else if (minProjected < 120)
284
286
  label = 'elevated';
285
287
  }
286
288
  return {