myrlin-workbook 0.8.3 → 0.8.4
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/package.json +1 -1
- package/src/state/store.js +3 -1
- package/src/web/pty-manager.js +40 -6
- package/src/web/public/index.html +3 -3
- package/src/web/server.js +48 -9
package/package.json
CHANGED
package/src/state/store.js
CHANGED
|
@@ -310,7 +310,7 @@ class Store extends EventEmitter {
|
|
|
310
310
|
|
|
311
311
|
// ─── Session CRUD ────────────────────────────────────────
|
|
312
312
|
|
|
313
|
-
createSession({ name, workspaceId, workingDir = '', topic = '', command = 'claude', resumeSessionId = null, tags = [] }) {
|
|
313
|
+
createSession({ name, workspaceId, workingDir = '', topic = '', command = 'claude', resumeSessionId = null, tags = [], initialPrompt = null, flags = [] }) {
|
|
314
314
|
if (!this._state.workspaces[workspaceId]) return null;
|
|
315
315
|
const id = crypto.randomUUID();
|
|
316
316
|
const now = new Date().toISOString();
|
|
@@ -325,6 +325,8 @@ class Store extends EventEmitter {
|
|
|
325
325
|
status: 'stopped', // 'running' | 'stopped' | 'error' | 'idle'
|
|
326
326
|
pid: null,
|
|
327
327
|
tags: Array.isArray(tags) ? tags : [],
|
|
328
|
+
initialPrompt: initialPrompt || null,
|
|
329
|
+
flags: Array.isArray(flags) ? flags : [],
|
|
328
330
|
createdAt: now,
|
|
329
331
|
lastActive: now,
|
|
330
332
|
logs: [],
|
package/src/web/pty-manager.js
CHANGED
|
@@ -125,7 +125,7 @@ class PtySessionManager {
|
|
|
125
125
|
* @param {boolean} [options.bypassPermissions=false] - If true, adds --dangerously-skip-permissions
|
|
126
126
|
* @returns {PtySession} The PTY session object
|
|
127
127
|
*/
|
|
128
|
-
spawnSession(sessionId, { command = 'claude', cwd, cols = 120, rows = 30, bypassPermissions = false, resumeSessionId = null, verbose = false, model = null, agentTeams = false, shell: requestedShell = null, newSession = false } = {}) {
|
|
128
|
+
spawnSession(sessionId, { command = 'claude', cwd, cols = 120, rows = 30, bypassPermissions = false, resumeSessionId = null, verbose = false, model = null, agentTeams = false, shell: requestedShell = null, newSession = false, initialPrompt = null, flags = [] } = {}) {
|
|
129
129
|
// Return existing session if already alive
|
|
130
130
|
const existing = this.sessions.get(sessionId);
|
|
131
131
|
if (existing && existing.alive) {
|
|
@@ -154,11 +154,30 @@ class PtySessionManager {
|
|
|
154
154
|
if (resumeSessionId) {
|
|
155
155
|
fullCommand += ' --resume ' + resumeSessionId;
|
|
156
156
|
} else if (cwd && !newSession) {
|
|
157
|
-
//
|
|
158
|
-
//
|
|
159
|
-
//
|
|
160
|
-
//
|
|
161
|
-
|
|
157
|
+
// Only add --continue when there is actually conversation history for this
|
|
158
|
+
// working directory. `claude --continue` exits with code 1 ("No conversation
|
|
159
|
+
// found to continue") on a fresh directory — e.g. a brand-new git worktree.
|
|
160
|
+
// Scan ~/.claude/projects/ for any JSONL file whose encoded path matches cwd.
|
|
161
|
+
let hasHistory = false;
|
|
162
|
+
try {
|
|
163
|
+
const claudeDir = path.join(os.homedir(), '.claude', 'projects');
|
|
164
|
+
if (fs.existsSync(claudeDir)) {
|
|
165
|
+
const normalizedCwd = cwd.replace(/[/\\]/g, path.sep);
|
|
166
|
+
const match = fs.readdirSync(claudeDir).find(d => {
|
|
167
|
+
try {
|
|
168
|
+
return decodeURIComponent(d).replace(/[/\\]/g, path.sep) === normalizedCwd;
|
|
169
|
+
} catch (_) { return false; }
|
|
170
|
+
});
|
|
171
|
+
if (match) {
|
|
172
|
+
const projDir = path.join(claudeDir, match);
|
|
173
|
+
hasHistory = fs.readdirSync(projDir).some(f => f.endsWith('.jsonl'));
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
} catch (_) { /* filesystem error — fall through, don't add --continue */ }
|
|
177
|
+
if (hasHistory) {
|
|
178
|
+
fullCommand += ' --continue';
|
|
179
|
+
}
|
|
180
|
+
// If no history, run bare `claude` — starts a fresh session without error.
|
|
162
181
|
}
|
|
163
182
|
if (bypassPermissions) {
|
|
164
183
|
fullCommand += ' --dangerously-skip-permissions';
|
|
@@ -169,6 +188,19 @@ class PtySessionManager {
|
|
|
169
188
|
if (model) {
|
|
170
189
|
fullCommand += ' --model ' + model;
|
|
171
190
|
}
|
|
191
|
+
// Extra flags (e.g. from worktree task checkboxes), validated upstream
|
|
192
|
+
if (Array.isArray(flags)) {
|
|
193
|
+
for (const f of flags) {
|
|
194
|
+
if (f && /^[a-zA-Z0-9-]+$/.test(f)) {
|
|
195
|
+
fullCommand += ' --' + f;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
// Initial prompt: appended as the last argument on first launch only
|
|
200
|
+
if (initialPrompt && typeof initialPrompt === 'string') {
|
|
201
|
+
const escaped = initialPrompt.replace(/'/g, "'\\''");
|
|
202
|
+
fullCommand += " '" + escaped + "'";
|
|
203
|
+
}
|
|
172
204
|
|
|
173
205
|
// Validate cwd exists. If the provided path is invalid (e.g. an encoded
|
|
174
206
|
// directory name like "-Users-jane-project"), resolve the real cwd from
|
|
@@ -479,6 +511,8 @@ class PtySessionManager {
|
|
|
479
511
|
model: storeSession.model || null,
|
|
480
512
|
agentTeams: storeSession.agentTeams || false,
|
|
481
513
|
resumeSessionId: storeSession.resumeSessionId || null,
|
|
514
|
+
initialPrompt: storeSession.resumeSessionId ? null : (storeSession.initialPrompt || null),
|
|
515
|
+
flags: storeSession.resumeSessionId ? [] : (storeSession.flags || []),
|
|
482
516
|
...spawnOpts,
|
|
483
517
|
});
|
|
484
518
|
} else {
|
|
@@ -1239,9 +1239,9 @@
|
|
|
1239
1239
|
<div class="form-group">
|
|
1240
1240
|
<label class="form-label">Flags</label>
|
|
1241
1241
|
<div class="form-checkboxes" id="new-task-flags">
|
|
1242
|
-
<label class="form-checkbox"><input type="checkbox" value="
|
|
1243
|
-
<label class="form-checkbox"><input type="checkbox" value="
|
|
1244
|
-
<label class="form-checkbox"><input type="checkbox" value="
|
|
1242
|
+
<label class="form-checkbox"><input type="checkbox" value="dangerously-skip-permissions" /> Skip Permissions</label>
|
|
1243
|
+
<label class="form-checkbox"><input type="checkbox" value="verbose" /> Verbose</label>
|
|
1244
|
+
<label class="form-checkbox"><input type="checkbox" value="agent-teams" id="new-task-agent-teams" /> Agent Teams</label>
|
|
1245
1245
|
</div>
|
|
1246
1246
|
</div>
|
|
1247
1247
|
<details class="form-help-details" style="margin-top: 4px; margin-bottom: 8px;">
|
package/src/web/server.js
CHANGED
|
@@ -3219,7 +3219,8 @@ app.post('/api/sessions/:id/spinoff-batch', requireAuth, async (req, res) => {
|
|
|
3219
3219
|
|
|
3220
3220
|
// Create session
|
|
3221
3221
|
const sessionName = (t.branch || t.title).replace(/^feat\//, '') + ' (spinoff)';
|
|
3222
|
-
const newSession = store.createSession(
|
|
3222
|
+
const newSession = store.createSession({
|
|
3223
|
+
workspaceId,
|
|
3223
3224
|
name: sessionName,
|
|
3224
3225
|
workingDir: worktreePath,
|
|
3225
3226
|
command: 'claude',
|
|
@@ -4767,7 +4768,7 @@ app.get('/api/worktree-tasks', requireAuth, async (req, res) => {
|
|
|
4767
4768
|
* Create a worktree task: creates git worktree, session, and task record.
|
|
4768
4769
|
*/
|
|
4769
4770
|
app.post('/api/worktree-tasks', requireAuth, async (req, res) => {
|
|
4770
|
-
const { workspaceId, repoDir, branch, description, baseBranch, featureId, model, tags, startNow } = req.body || {};
|
|
4771
|
+
const { workspaceId, repoDir, branch, description, baseBranch, featureId, model, tags, startNow, prompt, flags } = req.body || {};
|
|
4771
4772
|
if (!workspaceId) return res.status(400).json({ error: 'workspaceId is required' });
|
|
4772
4773
|
if (!repoDir) return res.status(400).json({ error: 'repoDir is required' });
|
|
4773
4774
|
if (!branch) return res.status(400).json({ error: 'branch is required' });
|
|
@@ -4807,7 +4808,7 @@ app.post('/api/worktree-tasks', requireAuth, async (req, res) => {
|
|
|
4807
4808
|
const root = await gitRepoRoot(repoDir);
|
|
4808
4809
|
if (!root) return res.status(400).json({ error: 'Not a git repository' });
|
|
4809
4810
|
const repoName = path.basename(root);
|
|
4810
|
-
|
|
4811
|
+
let worktreePath = path.join(path.dirname(root), `${repoName}-wt`, branch.replace(/\//g, '-'));
|
|
4811
4812
|
|
|
4812
4813
|
let branchExists = false;
|
|
4813
4814
|
try {
|
|
@@ -4815,11 +4816,44 @@ app.post('/api/worktree-tasks', requireAuth, async (req, res) => {
|
|
|
4815
4816
|
branchExists = true;
|
|
4816
4817
|
} catch {}
|
|
4817
4818
|
|
|
4818
|
-
|
|
4819
|
-
|
|
4820
|
-
|
|
4821
|
-
|
|
4822
|
-
|
|
4819
|
+
// Check existing worktrees to avoid two fatal git errors:
|
|
4820
|
+
// 1. "already exists" — target path is already a registered worktree
|
|
4821
|
+
// 2. "already checked out" — the branch is checked out in a different worktree
|
|
4822
|
+
// Parse `git worktree list --porcelain` once and handle both cases.
|
|
4823
|
+
let skipWorktreeAdd = false;
|
|
4824
|
+
try {
|
|
4825
|
+
const listOut = await gitExec(['worktree', 'list', '--porcelain'], root);
|
|
4826
|
+
|
|
4827
|
+
// Case 1: exact path already registered — reuse it as-is
|
|
4828
|
+
if (listOut.includes(`worktree ${worktreePath}`)) {
|
|
4829
|
+
skipWorktreeAdd = true;
|
|
4830
|
+
}
|
|
4831
|
+
|
|
4832
|
+
// Case 2: branch already checked out in a *different* worktree path —
|
|
4833
|
+
// redirect worktreePath to that existing location so the rest of task
|
|
4834
|
+
// creation (session, record) still succeeds pointing at the right dir.
|
|
4835
|
+
if (!skipWorktreeAdd) {
|
|
4836
|
+
const branchRef = `refs/heads/${branch}`;
|
|
4837
|
+
const blocks = listOut.split('\n\n').filter(Boolean);
|
|
4838
|
+
for (const block of blocks) {
|
|
4839
|
+
const pathMatch = block.match(/^worktree (.+)$/m);
|
|
4840
|
+
const branchMatch = block.match(/^branch (.+)$/m);
|
|
4841
|
+
if (pathMatch && branchMatch && branchMatch[1].trim() === branchRef) {
|
|
4842
|
+
worktreePath = pathMatch[1].trim();
|
|
4843
|
+
skipWorktreeAdd = true;
|
|
4844
|
+
break;
|
|
4845
|
+
}
|
|
4846
|
+
}
|
|
4847
|
+
}
|
|
4848
|
+
} catch {}
|
|
4849
|
+
|
|
4850
|
+
if (!skipWorktreeAdd) {
|
|
4851
|
+
const args = ['worktree', 'add'];
|
|
4852
|
+
if (!branchExists) args.push('-b', branch);
|
|
4853
|
+
args.push(worktreePath);
|
|
4854
|
+
if (branchExists) args.push(branch);
|
|
4855
|
+
await gitExec(args, root);
|
|
4856
|
+
}
|
|
4823
4857
|
|
|
4824
4858
|
// 1.5. Run init hooks (copy_files and init_script) if configured
|
|
4825
4859
|
const initHooks = store.getWorktreeInitHooks();
|
|
@@ -4855,11 +4889,16 @@ app.post('/api/worktree-tasks', requireAuth, async (req, res) => {
|
|
|
4855
4889
|
|
|
4856
4890
|
// 2. Create a session in this workspace pointing at the worktree
|
|
4857
4891
|
const sessionName = branch.replace(/^feat\//, '') + ' (worktree task)';
|
|
4858
|
-
const
|
|
4892
|
+
const safePrompt = (typeof prompt === 'string' && prompt.trim()) ? prompt.trim() : null;
|
|
4893
|
+
const safeFlags = Array.isArray(flags) ? flags.filter(f => typeof f === 'string' && /^[a-zA-Z0-9-]+$/.test(f)) : [];
|
|
4894
|
+
const session = store.createSession({
|
|
4895
|
+
workspaceId,
|
|
4859
4896
|
name: sessionName,
|
|
4860
4897
|
workingDir: worktreePath,
|
|
4861
4898
|
command: 'claude',
|
|
4862
4899
|
model: model || undefined,
|
|
4900
|
+
initialPrompt: safePrompt,
|
|
4901
|
+
flags: safeFlags,
|
|
4863
4902
|
});
|
|
4864
4903
|
if (!session) {
|
|
4865
4904
|
return res.status(500).json({ error: 'Failed to create session' });
|