@bakapiano/ccsm 0.21.3 → 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 +11 -1
- package/public/js/backend.js +39 -0
- package/public/js/components/TerminalView.js +14 -0
- package/public/js/pages/ConfigurePage.js +9 -1
- package/public/js/pages/SessionsPage.js +14 -5
- package/public/js/streaming.js +5 -2
- package/server.js +44 -1
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
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
import { signal } from '@preact/signals';
|
|
5
5
|
import * as S from './state.js';
|
|
6
|
-
import { httpBase, getToken, getDeviceId, getDeviceCode, isRemoteAccess } from './backend.js';
|
|
6
|
+
import { httpBase, getToken, getDeviceId, getDeviceCode, isRemoteAccess, estimateTermSize } from './backend.js';
|
|
7
7
|
|
|
8
8
|
// Global pending-approval signal. Flipped to true whenever any /api
|
|
9
9
|
// call returns 403 {pending:true}; PendingApprovalOverlay watches this
|
|
@@ -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
|
|
@@ -286,6 +293,9 @@ export function resumeSession(sessionId) {
|
|
|
286
293
|
// Resolved terminal theme → backend sets a matching COLORFGBG so the
|
|
287
294
|
// CLI's light/dark auto-detection follows the ccsm terminal.
|
|
288
295
|
theme: document.documentElement.dataset.theme,
|
|
296
|
+
// Seed the PTY at the pane's real size so alt-screen CLIs (claude)
|
|
297
|
+
// don't lay out at node-pty's 30-row default and get stranded short.
|
|
298
|
+
...(estimateTermSize() || {}),
|
|
289
299
|
});
|
|
290
300
|
await loadSessions();
|
|
291
301
|
return r.launched;
|
package/public/js/backend.js
CHANGED
|
@@ -83,6 +83,45 @@ export function getDeviceId() {
|
|
|
83
83
|
}
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
+
// ── Initial terminal geometry ─────────────────────────────────────
|
|
87
|
+
// Estimate how many cols/rows the live session pane can hold, so a
|
|
88
|
+
// resumed / newly-launched PTY can spawn at roughly the right size
|
|
89
|
+
// instead of node-pty's 120×30 default. Why it matters: an alt-screen
|
|
90
|
+
// TUI like claude lays its entire UI out the instant it starts, using
|
|
91
|
+
// whatever size the PTY had then. xterm only sends the real size once
|
|
92
|
+
// its WebSocket opens — a beat later — and claude, having already
|
|
93
|
+
// painted at 30 rows, doesn't re-expand to fill a tall window until
|
|
94
|
+
// something forces a redraw (e.g. the user resizing it). On a big
|
|
95
|
+
// display that strands the terminal at ~1/4 height. Seeding the spawn
|
|
96
|
+
// with the pane's real dimensions sidesteps the race; xterm's own fit
|
|
97
|
+
// still corrects any few-row estimate error when it attaches.
|
|
98
|
+
// Returns null when nothing measurable is mounted, so the caller omits
|
|
99
|
+
// the hint and the backend keeps its default.
|
|
100
|
+
export function estimateTermSize() {
|
|
101
|
+
let w, h;
|
|
102
|
+
const pane = document.querySelector('.terminal-host')
|
|
103
|
+
|| document.querySelector('.session-pane-body');
|
|
104
|
+
if (pane) {
|
|
105
|
+
const r = pane.getBoundingClientRect();
|
|
106
|
+
w = r.width; h = r.height;
|
|
107
|
+
} else {
|
|
108
|
+
// Launching from the Launch page — no pane yet. Approximate from the
|
|
109
|
+
// window minus the sidebar column and the ~70px of top chrome.
|
|
110
|
+
const sb = document.querySelector('.sidebar');
|
|
111
|
+
w = window.innerWidth - (sb ? sb.getBoundingClientRect().width : 232) - 32;
|
|
112
|
+
h = window.innerHeight - 70;
|
|
113
|
+
}
|
|
114
|
+
if (!(w > 40) || !(h > 40)) return null;
|
|
115
|
+
// Mirror TerminalView's font sizing (13px desktop / 11px mobile,
|
|
116
|
+
// lineHeight 1.2); cell advance ≈ 0.6em for the mono stack.
|
|
117
|
+
const isMobile = window.matchMedia('(max-width: 640px)').matches;
|
|
118
|
+
const fontSize = isMobile ? 11 : 13;
|
|
119
|
+
return {
|
|
120
|
+
cols: Math.max(20, Math.min(400, Math.floor(w / (fontSize * 0.6)))),
|
|
121
|
+
rows: Math.max(8, Math.min(200, Math.floor(h / (fontSize * 1.2)))),
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
86
125
|
// Per-device 4-digit human-verification code. Sent alongside the
|
|
87
126
|
// device id so the operator approving on the host can match what
|
|
88
127
|
// they see in the Remote page against what the requesting user
|
|
@@ -229,6 +229,20 @@ export function TerminalView({ terminalId, cliType }) {
|
|
|
229
229
|
scheduleFit();
|
|
230
230
|
termRef.current = term;
|
|
231
231
|
|
|
232
|
+
// Web fonts settle AFTER the first fit. The terminal's mono stack
|
|
233
|
+
// (`Geist Mono` / `JetBrains Mono`) loads async from Google Fonts with
|
|
234
|
+
// display:swap, so on a machine without a local `Cascadia Mono` the very
|
|
235
|
+
// first term.open()+fit() measures cell metrics against the fallback font.
|
|
236
|
+
// When the real font swaps in its cell height changes, making the row
|
|
237
|
+
// count fit computed wrong — and because the host's box size never changed,
|
|
238
|
+
// the ResizeObserver never fires to correct it. The terminal ends up a row
|
|
239
|
+
// or two short of (or past) the host, "sometimes" — depending purely on
|
|
240
|
+
// whether the font was already cached. Re-fit once fonts settle; it's a
|
|
241
|
+
// no-op when the metrics didn't actually change (e.g. local font hit).
|
|
242
|
+
try {
|
|
243
|
+
document.fonts?.ready?.then(() => { if (termRef.current === term) scheduleFit(); });
|
|
244
|
+
} catch {}
|
|
245
|
+
|
|
232
246
|
// Browser WS API can't set Authorization headers — token + device
|
|
233
247
|
// ride as query string when we have them (Remote-mode access).
|
|
234
248
|
// Server's upgrade handler reads both when Host is non-loopback.
|
|
@@ -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/public/js/streaming.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Items live in a signal keyed by repo so progress rows are reactive.
|
|
3
3
|
|
|
4
4
|
import { signal } from '@preact/signals';
|
|
5
|
-
import { httpBase } from './backend.js';
|
|
5
|
+
import { httpBase, estimateTermSize } from './backend.js';
|
|
6
6
|
|
|
7
7
|
// progressByContext[rootId] = { repoName: { phase, percent, detail, state, indeterminate, name } }
|
|
8
8
|
export const progressByContext = signal({});
|
|
@@ -63,10 +63,13 @@ export async function streamNewSession(body, { progressRootId = 'newSessionProgr
|
|
|
63
63
|
// Pass the resolved terminal theme so the backend can hand CLIs a matching
|
|
64
64
|
// COLORFGBG (light/dark detection). dataset.theme is set by applyTheme().
|
|
65
65
|
const theme = document.documentElement.dataset.theme;
|
|
66
|
+
// Seed the PTY at the pane's real size (estimated from the window here,
|
|
67
|
+
// since the terminal isn't mounted yet) so alt-screen CLIs don't start
|
|
68
|
+
// at node-pty's 30-row default and get stranded short of a tall window.
|
|
66
69
|
const res = await fetch(httpBase() + '/api/sessions/new', {
|
|
67
70
|
method: 'POST',
|
|
68
71
|
headers: { 'Content-Type': 'application/json' },
|
|
69
|
-
body: JSON.stringify({ ...body, theme }),
|
|
72
|
+
body: JSON.stringify({ ...body, theme, ...(estimateTermSize() || {}) }),
|
|
70
73
|
});
|
|
71
74
|
if (!res.ok && res.headers.get('content-type')?.startsWith('application/json')) {
|
|
72
75
|
const j = await res.json();
|
package/server.js
CHANGED
|
@@ -281,7 +281,7 @@ function quoteForCmd(s) {
|
|
|
281
281
|
return s;
|
|
282
282
|
}
|
|
283
283
|
|
|
284
|
-
function spawnCliSession({ cli, cwd, sessionId, meta, extraArgs = [], theme }) {
|
|
284
|
+
function spawnCliSession({ cli, cwd, sessionId, meta, extraArgs = [], theme, cols, rows }) {
|
|
285
285
|
if (!webTerminal.available) {
|
|
286
286
|
const e = new Error('node-pty unavailable · cannot spawn web terminal');
|
|
287
287
|
e.code = 'PTY_UNAVAILABLE';
|
|
@@ -331,12 +331,22 @@ function spawnCliSession({ cli, cwd, sessionId, meta, extraArgs = [], theme }) {
|
|
|
331
331
|
if (theme === 'light' || theme === 'dark') {
|
|
332
332
|
env.COLORFGBG = theme === 'light' ? '0;15' : '15;0';
|
|
333
333
|
}
|
|
334
|
+
// Spawn the PTY at the size the frontend measured for its terminal pane
|
|
335
|
+
// (clamped against junk), so alt-screen CLIs lay out at the right height
|
|
336
|
+
// from the first frame instead of node-pty's 120×30 default. Omitted ⇒
|
|
337
|
+
// webTerminal.spawn keeps its default; xterm's first resize corrects any
|
|
338
|
+
// small estimate error on attach regardless.
|
|
339
|
+
const sized = (Number(cols) > 0 && Number(rows) > 0)
|
|
340
|
+
? { cols: Math.min(400, Math.max(20, Math.floor(Number(cols)))),
|
|
341
|
+
rows: Math.min(200, Math.max(8, Math.floor(Number(rows)))) }
|
|
342
|
+
: {};
|
|
334
343
|
const trySpawn = (executable) => webTerminal.spawn({
|
|
335
344
|
id: sessionId,
|
|
336
345
|
command: executable,
|
|
337
346
|
args,
|
|
338
347
|
cwd,
|
|
339
348
|
env,
|
|
349
|
+
...sized,
|
|
340
350
|
meta: { ...meta, cliId: cli.id, cliName: cli.name },
|
|
341
351
|
onData: () => {
|
|
342
352
|
persistedSessions.touch(sessionId).catch(() => {});
|
|
@@ -623,6 +633,35 @@ app.delete('/api/sessions/:id', asyncH(async (req, res) => {
|
|
|
623
633
|
res.json({ removed });
|
|
624
634
|
}));
|
|
625
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
|
+
|
|
626
665
|
// Reorder sessions within a folder. Body: { folderId, ids } where ids
|
|
627
666
|
// is the new sequence of session ids in their final display order
|
|
628
667
|
// inside that folder. Each session gets `folderId` + `order: 0..N-1`
|
|
@@ -867,6 +906,8 @@ app.post('/api/sessions/new', async (req, res) => {
|
|
|
867
906
|
meta: { title: workspace.name, workspace: workspace.name, cwd: workspace.path },
|
|
868
907
|
extraArgs: [...themeArgs, ...newSessionArgs],
|
|
869
908
|
theme: req.body && req.body.theme,
|
|
909
|
+
cols: req.body && req.body.cols,
|
|
910
|
+
rows: req.body && req.body.rows,
|
|
870
911
|
});
|
|
871
912
|
await persistedSessions.markRunning(record.id, entry.meta.pid);
|
|
872
913
|
launched = { id: record.id, pid: entry.meta.pid, cliId: cli.id };
|
|
@@ -1004,6 +1045,8 @@ app.post('/api/sessions/:id/resume', asyncH(async (req, res) => {
|
|
|
1004
1045
|
meta: { title: record.title || record.workspace, workspace: record.workspace, cwd: record.cwd },
|
|
1005
1046
|
extraArgs: [...themeArgs, ...extraArgs],
|
|
1006
1047
|
theme: req.body && req.body.theme,
|
|
1048
|
+
cols: req.body && req.body.cols,
|
|
1049
|
+
rows: req.body && req.body.rows,
|
|
1007
1050
|
});
|
|
1008
1051
|
await persistedSessions.markRunning(record.id, entry.meta.pid);
|
|
1009
1052
|
res.json({ launched: { id: record.id, pid: entry.meta.pid, cliId: cli.id } });
|