@bakapiano/ccsm 0.21.4 → 0.21.5

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/lib/config.js CHANGED
@@ -80,6 +80,11 @@ const DEFAULTS = {
80
80
  // Launch button when the user doesn't override.
81
81
  clis: DEFAULT_CLIS,
82
82
  defaultCliId: 'claude',
83
+ // External editor command for the "Open in editor" session action.
84
+ // Spawned as `<editor> "<cwd>"`; default `code` = VS Code (whose Source
85
+ // Control panel doubles as the review-changes view once the folder's
86
+ // open). Point it at `cursor`, `code-insiders`, `subl`, … as desired.
87
+ editor: 'code',
83
88
  // Devtunnel state. tunnelId holds the persistent (named) tunnel
84
89
  // ccsm minted via `devtunnel create` on first Start. Reusing it
85
90
  // across host restarts keeps the public URL — and therefore the
@@ -135,6 +140,7 @@ function mergeWithDefaults(partial) {
135
140
 
136
141
  if (!Array.isArray(out.repos)) out.repos = DEFAULTS.repos;
137
142
  if (!Array.isArray(out.clis)) out.clis = [];
143
+ if (typeof out.editor !== 'string') out.editor = DEFAULTS.editor;
138
144
  // Always inject builtin CLIs (claude, codex) if they're missing or were
139
145
  // deleted from a saved config — they're managed by ccsm, the user can
140
146
  // tweak args/shell but can't remove them. Preserves any user
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bakapiano/ccsm",
3
- "version": "0.21.4",
3
+ "version": "0.21.5",
4
4
  "description": "Claude Code Session Manager — Windows web UI to manage many concurrent claude sessions: live list, snapshot/restore, focus existing window, new session in an isolated workspace with repo clones",
5
5
  "license": "MIT",
6
6
  "main": "server.js",
@@ -349,12 +349,17 @@
349
349
  display: inline-flex;
350
350
  align-items: center;
351
351
  justify-content: center;
352
- color: #fff;
352
+ /* Follow the terminal foreground so the dots read on both the light
353
+ (#f0f0f0) and dark (#252526) tab strip. The old hardcoded #fff was
354
+ invisible on the light strip. */
355
+ color: var(--term-on);
353
356
  cursor: pointer;
354
357
  flex-shrink: 0;
355
358
  transition: background-color .12s, color .12s;
356
359
  }
357
- .session-menu-btn:hover { background: rgba(255, 255, 255, 0.1); color: #fff; }
360
+ /* Neutral-grey hover tint works on either strip colour (darkens the light
361
+ one, lightens the dark one) without needing a per-theme override. */
362
+ .session-menu-btn:hover { background: rgba(128, 128, 128, 0.2); color: var(--term-on); }
358
363
  .session-menu-btn svg { width: 16px; height: 16px; }
359
364
 
360
365
  .session-menu {
package/public/js/api.js CHANGED
@@ -259,6 +259,13 @@ export async function deleteSession(sessionId) {
259
259
  await loadSessions();
260
260
  }
261
261
 
262
+ // Open the session's working directory in the user's configured editor
263
+ // (Settings → Editor, default `code`). Returns { editor, cwd } so the
264
+ // caller can surface which editor it launched.
265
+ export function openSessionInEditor(sessionId) {
266
+ return api('POST', `/api/sessions/${sessionId}/open-editor`);
267
+ }
268
+
262
269
  // Per-session in-flight resume promise. Sidebar.onClick and the
263
270
  // SessionsPage auto-resume effect can both fire for the same exited
264
271
  // session in the same tick (clicking an exited row mounts SessionsPage
@@ -154,7 +154,7 @@ export function ConfigurePage() {
154
154
 
155
155
  useEffect(() => {
156
156
  if (cfg && !general) {
157
- setGeneral({ workDir: cfg.workDir });
157
+ setGeneral({ workDir: cfg.workDir, editor: cfg.editor });
158
158
  }
159
159
  }, [cfg]);
160
160
 
@@ -167,6 +167,7 @@ export function ConfigurePage() {
167
167
  const saved = await api('PUT', '/api/config', {
168
168
  ...cfg,
169
169
  workDir: (merged.workDir || '').trim(),
170
+ editor: (merged.editor || '').trim(),
170
171
  });
171
172
  config.value = saved;
172
173
  setToast('saved');
@@ -198,6 +199,13 @@ export function ConfigurePage() {
198
199
  <span class="label">Backend</span>
199
200
  <${RestartButton} />
200
201
  </div>
202
+ <label class="field">
203
+ <span class="label">Editor</span>
204
+ <input type="text" class="mono" value=${general.editor || ''}
205
+ placeholder="code"
206
+ onChange=${(e) => saveGeneral({ editor: e.target.value })} />
207
+ <span class="hint">Command for a session's “Open in editor” action. Default <code>code</code> (VS Code). Try <code>cursor</code>, <code>code-insiders</code>, …</span>
208
+ </label>
201
209
  </div>
202
210
  </${Section}>
203
211
 
@@ -7,13 +7,13 @@
7
7
  import { html } from '../html.js';
8
8
  import { useEffect, useRef, useState } from 'preact/hooks';
9
9
  import { activeSessionId, sessions, config, selectTab, selectSession, clockTick } from '../state.js';
10
- import { resumeSession, clearResumeFailure, deleteSession, setSessionTitle } from '../api.js';
10
+ import { resumeSession, clearResumeFailure, deleteSession, setSessionTitle, openSessionInEditor } from '../api.js';
11
11
  import { setToast } from '../toast.js';
12
12
  import { ccsmConfirm, ccsmPrompt } from '../dialog.js';
13
13
  import { TerminalView } from '../components/TerminalView.js';
14
14
  import { PageTitleBar } from '../components/PageTitleBar.js';
15
15
  import { Popover } from '../components/Popover.js';
16
- import { IconMoreVert, IconPencil, IconClose, IconPlus, IconForCliType, IconTerminal } from '../icons.js';
16
+ import { IconMoreVert, IconPencil, IconClose, IconPlus, IconForCliType, IconTerminal, IconExternal } from '../icons.js';
17
17
  import { fmtAgo } from '../util.js';
18
18
 
19
19
  function SessionTabs({ activeId, onActivate, onNew, kebab }) {
@@ -51,7 +51,7 @@ function SessionTabs({ activeId, onActivate, onNew, kebab }) {
51
51
  </div>`;
52
52
  }
53
53
 
54
- function SessionMenu({ session, onRename, onDelete }) {
54
+ function SessionMenu({ session, onRename, onDelete, onOpenEditor }) {
55
55
  const [open, setOpen] = useState(false);
56
56
  const anchor = useRef(null);
57
57
  return html`
@@ -61,9 +61,12 @@ function SessionMenu({ session, onRename, onDelete }) {
61
61
  <${IconMoreVert} />
62
62
  </button>
63
63
  ${open ? html`
64
- <${Popover} anchor=${anchor} align="right" width=${180}
64
+ <${Popover} anchor=${anchor} align="right" width=${200}
65
65
  onClose=${() => setOpen(false)}>
66
66
  <div class="session-menu">
67
+ <button class="session-menu-item" onClick=${() => { setOpen(false); onOpenEditor(); }}>
68
+ <${IconExternal} /> Open in editor
69
+ </button>
67
70
  <button class="session-menu-item" onClick=${() => { setOpen(false); onRename(); }}>
68
71
  <${IconPencil} /> Rename
69
72
  </button>
@@ -129,6 +132,12 @@ export function SessionsPage() {
129
132
  activeSessionId.value = null;
130
133
  } catch (e) { setToast(e.message, 'error'); }
131
134
  };
135
+ const onOpenEditor = async () => {
136
+ try {
137
+ const r = await openSessionInEditor(session.id);
138
+ setToast(`Opening in ${r?.editor || 'editor'}…`);
139
+ } catch (e) { setToast(e.message, 'error'); }
140
+ };
132
141
 
133
142
  return html`
134
143
  <${PageTitleBar} title=${html`
@@ -146,7 +155,7 @@ export function SessionsPage() {
146
155
  activeId=${session.id}
147
156
  onActivate=${(sid) => selectSession(sid)}
148
157
  onNew=${() => selectTab('launch')}
149
- kebab=${html`<${SessionMenu} session=${session} onRename=${onRename} onDelete=${onDelete} />`} />
158
+ kebab=${html`<${SessionMenu} session=${session} onRename=${onRename} onDelete=${onDelete} onOpenEditor=${onOpenEditor} />`} />
150
159
  <div class="session-pane">
151
160
  <div class="session-pane-body">
152
161
  ${running
package/server.js CHANGED
@@ -633,6 +633,35 @@ app.delete('/api/sessions/:id', asyncH(async (req, res) => {
633
633
  res.json({ removed });
634
634
  }));
635
635
 
636
+ // Open a session's working directory in the user's configured editor
637
+ // (config.editor, default `code` = VS Code, whose Source Control panel is
638
+ // also the review-changes view once the folder's open). Spawned detached
639
+ // so it outlives ccsm; shell:true so Windows resolves `code.cmd` via
640
+ // PATHEXT and a command like `code --reuse-window` parses, with the cwd
641
+ // quoted so paths with spaces survive the shell. spawnEnv() merges the
642
+ // user-scope PATH so `code`/`cursor` are found even when the inherited
643
+ // env lacks them.
644
+ app.post('/api/sessions/:id/open-editor', asyncH(async (req, res) => {
645
+ const record = await persistedSessions.get(req.params.id);
646
+ if (!record) return res.status(404).json({ error: 'session not found' });
647
+ const cfg = await loadConfig();
648
+ const editor = (cfg.editor || '').trim() || 'code';
649
+ const { spawn } = require('node:child_process');
650
+ try {
651
+ const child = spawn(editor, [`"${record.cwd}"`], {
652
+ detached: true, stdio: 'ignore', shell: true,
653
+ env: spawnEnv(), windowsHide: true,
654
+ });
655
+ // A bad editor command fails the shell async (after we've responded);
656
+ // log it so it's diagnosable, but the happy path needs no await.
657
+ child.on('error', (e) => console.warn(`[ccsm] open-editor "${editor}" failed:`, e.message));
658
+ child.unref();
659
+ res.json({ ok: true, editor, cwd: record.cwd });
660
+ } catch (e) {
661
+ res.status(500).json({ error: `failed to launch ${editor}: ${e.message}` });
662
+ }
663
+ }));
664
+
636
665
  // Reorder sessions within a folder. Body: { folderId, ids } where ids
637
666
  // is the new sequence of session ids in their final display order
638
667
  // inside that folder. Each session gets `folderId` + `order: 0..N-1`