@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 +6 -0
- package/package.json +1 -1
- package/public/css/terminals.css +7 -2
- package/public/js/api.js +7 -0
- package/public/js/pages/ConfigurePage.js +9 -1
- package/public/js/pages/SessionsPage.js +14 -5
- package/server.js +29 -0
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.
|
|
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",
|
package/public/css/terminals.css
CHANGED
|
@@ -349,12 +349,17 @@
|
|
|
349
349
|
display: inline-flex;
|
|
350
350
|
align-items: center;
|
|
351
351
|
justify-content: center;
|
|
352
|
-
|
|
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
|
-
|
|
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=${
|
|
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`
|