@galda/cli 0.10.79 → 0.10.81

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/app/index.html CHANGED
@@ -6828,7 +6828,10 @@ function renderFsBoard(){
6828
6828
  // errored is status-based now (blocked/failed/partial/interrupted live in the Needs-you column,
6829
6829
  // not Doing). Doing holds only what's actively moving, so bkt==='doing' ⇒ healthy in flight.
6830
6830
  const errored = ['blocked', 'failed', 'partial', 'interrupted'].includes(g.status);
6831
- const intr = g.status === 'interrupted';
6831
+ // `partial` can mean an earlier step completed and the latest one was
6832
+ // interrupted. Read the concrete task so this card doesn't turn amber,
6833
+ // resumable work into a red generic failure.
6834
+ const intr = g.status === 'interrupted' || list.some((t) => t.goalId === g.id && t.status === 'interrupted');
6832
6835
  const isPaused = g.status === 'blocked' && g.blocked?.kind === 'paused'; // 一時停止=エラーでなくアンバー扱い
6833
6836
  const amber = intr || isPaused;
6834
6837
  const healthyRun = bkt === 'doing' && !errored; // paused(=errored) goals now sit in 'doing' too but carry no progress bar
@@ -7161,7 +7164,10 @@ function errorButtonsHtml(meta, { taskId = null, goalId = null, interrupted = fa
7161
7164
  const out = [];
7162
7165
  for (const id of meta.actionIds || []) {
7163
7166
  if (id === 'retry' && taskId != null) out.push(`<button class="errbtn" data-fsretry="${taskId}">${interrupted ? C.resume : C.retry}</button>`);
7164
- else if (id === 'run-with-codex' && canCodex) out.push(`<button class="errbtn" data-fscodex="${goalId}">${C.other}</button>`);
7167
+ // This keeps the existing agent-switch endpoint, but says what it really does
7168
+ // for an interrupted run: continue the saved goal with another agent rather
7169
+ // than imply a brand-new, contextless task will be created.
7170
+ else if (id === 'run-with-codex' && canCodex) out.push(`<button class="errbtn" data-fscodex="${goalId}">${interrupted ? (failureLang() === 'ja' ? 'Codexで再開' : 'Resume with Codex') : C.other}</button>`);
7165
7171
  else if (id === 'connect') out.push(`<button class="errbtn" data-fsconnectworker="1">${C.connect}</button>`);
7166
7172
  else if (id === 'check-connection') out.push(`<button class="errbtn" data-fscheckworker="1">${C.check}</button>`);
7167
7173
  else if (id === 'retry-tests' && goalId != null) out.push(`<button class="errbtn" data-fsreverify="${goalId}">${C.tests}</button>`);
@@ -7297,10 +7303,6 @@ function renderFsTodo(){
7297
7303
  const needsCount = failed.filter((t) => !resumedGoalIds.has(t.goalId)).length + blocked.length + needsInput.length;
7298
7304
  const doingCount = running.length + finalizingGoals.length + pausedGoalIds.size;
7299
7305
  const nextCount = parkedReplyGoals.length + queued.length;
7300
- const hasPipelineWork = doingCount > 0 || nextCount > 0 || pgoals.some((g) => ['planning', 'stacked'].includes(g.status));
7301
- const laterIdle = later.length && !hasPipelineWork
7302
- ? `<div class="lateridle"><span class="msg">${document.documentElement.lang === 'ja' ? `実行待ちはありません。Laterの${later.length}件は自動開始されません。` : `Nothing is queued. ${later.length} Later item${later.length === 1 ? '' : 's'} will not start automatically.`}</span><button data-fsstartnextlater="${later[0].id}">${document.documentElement.lang === 'ja' ? '次を開始' : 'Start next'}</button></div>`
7303
- : '';
7304
7306
  // Free-tier pending counter (H22): on the free plan the header shows the wall
7305
7307
  // — pending goals out of the 5-slot cap — reddening at the cap and gone once
7306
7308
  // paid. Reads the SAME numbers the gate enforces (state.billing, never a local
@@ -7322,7 +7324,6 @@ function renderFsTodo(){
7322
7324
  <div class="qadd" data-fsnew="1" role="button" tabindex="0" title="New goal"><span class="paplus"></span><span>New</span></div>
7323
7325
  <span class="tlcap">Later</span>
7324
7326
  <div class="qadd lateradd" data-fslaternew="1" role="button" tabindex="0" title="New Later goal"><span class="paplus"></span><span>New</span></div>
7325
- ${laterIdle}
7326
7327
  ${laterRows}
7327
7328
  </div>
7328
7329
  ${doneAll.length ? `<div class="doneline${fsUI.doneOpen ? ' open' : ''}" id="fsDoneLine">✓ Done<span class="num">${doneAll.length}</span><span class="fold">▾</span></div>
@@ -7387,13 +7388,6 @@ function renderFsTodo(){
7387
7388
  let r; try { r = await fetch(withKey(`/api/goals/${b.dataset.fspstart}/activate`), { method: 'POST' }); }
7388
7389
  catch { showErr('Start failed.'); return; }
7389
7390
  if (!r.ok) showErr('Start failed.'); else await refresh(); };
7390
- const startNextLater = el.querySelector('[data-fsstartnextlater]');
7391
- if (startNextLater) startNextLater.onclick = async (e) => { e.stopPropagation();
7392
- startNextLater.disabled = true;
7393
- let r; try { r = await fetch(withKey(`/api/goals/${startNextLater.dataset.fsstartnextlater}/activate`), { method: 'POST' }); }
7394
- catch { showErr(document.documentElement.lang === 'ja' ? '開始できませんでした' : 'Could not start the next item.'); startNextLater.disabled = false; return; }
7395
- if (!r.ok) { showErr(document.documentElement.lang === 'ja' ? '開始できませんでした' : 'Could not start the next item.'); startNextLater.disabled = false; }
7396
- else await refresh(); };
7397
7391
  for (const b of el.querySelectorAll('[data-fspdel]')) b.onclick = async (e) => { e.stopPropagation();
7398
7392
  const preview = b.closest('.trow')?.querySelector('.tt')?.textContent || '';
7399
7393
  if (!(await confirmDelete({ title: 'Delete this shelved goal?', preview }))) return;
@@ -8933,7 +8927,14 @@ async function send(){
8933
8927
  // review goal → the AI decides continue/rescope/split/answer (same path as the
8934
8928
  // review card's own reply box), with the note anchored under the composer.
8935
8929
  const body = await replyToGoal(bind.goal.id, fullText, '#input');
8936
- if (body) { upsert(state.goals, body.goal); if (body.task) upsert(state.tasks, body.task); render(); }
8930
+ if (body) {
8931
+ upsert(state.goals, body.goal); if (body.task) upsert(state.tasks, body.task);
8932
+ // A Review reply is a one-shot handoff. Keeping the composer bound
8933
+ // afterwards adds a redundant “Replying to #N” banner and makes the
8934
+ // next, unrelated instruction easy to send to the old goal by mistake.
8935
+ state.selectedGoal = null;
8936
+ render();
8937
+ }
8937
8938
  } else {
8938
8939
  // running/partial/failed/interrupted/blocked → a plain follow-up on the same goal.
8939
8940
  const r = await fetch(withKey(`/api/goals/${bind.goal.id}/reply`), { method: 'POST',
@@ -9178,6 +9179,11 @@ function latestGoalRunForOutcome(goalId, tasks, outcome){
9178
9179
  function saRowFromDigest(r, gs, ts){
9179
9180
  const g = gs.find((x) => x.id === r.goalIds[0]);
9180
9181
  const outcome = latestGoalOutcomeTask(g?.id, ts);
9182
+ // A timeout commonly settles its parent goal as `partial`: some earlier work
9183
+ // may be real, while the final task is retryable. Preserve the task-level truth
9184
+ // here so the card never calls that state a generic failure.
9185
+ const interruptedTask = [...ts].filter((t) => r.goalIds.includes(t.goalId) && t.status === 'interrupted')
9186
+ .sort((a, b) => taskSortTime(b) - taskSortTime(a))[0] || null;
9181
9187
  const pt = ts.find((t) => r.goalIds.includes(t.goalId) && t.proof && (t.proof.png || t.proof.gif));
9182
9188
  // "What we did" (H23 §2, replaces the old Plan/Review-thread folds): one bullet
9183
9189
  // per finished requirement task's own report; falls back to the goal's plan
@@ -9207,6 +9213,7 @@ function saRowFromDigest(r, gs, ts){
9207
9213
  .map((x) => ({ id: x.id, title: saReviewTitle(x) }));
9208
9214
  return {
9209
9215
  ids: r.goalIds, goalId: r.goalIds[0], goal: g, folded,
9216
+ interruptedTask,
9210
9217
  no: r.reviewNumber,
9211
9218
  title: saReviewTitle(g),
9212
9219
  check: String(g?.reviewSummary?.check || r.checkLine || '').replace(/\s+/g, ' ').trim(),
@@ -9733,10 +9740,27 @@ function saItemHtml(it, i){
9733
9740
  ? `<div class="ropen rdeclerr"><p class="rbody v">${failureLang() === 'ja' ? 'AIが開けるものを用意しようとしましたが、記録が読み取れませんでした。返信して直させてください。' : 'The AI tried to declare something to open here, but the record was unreadable. Reply to have it rewritten.'}</p></div>`
9734
9741
  : '';
9735
9742
  const openBody = (it.run?.cmd || it.run?.url)
9736
- ? `<div class="ropen" id="runlocal${i}"><button type="button" class="olink" data-saact="preview:${i}" id="prevlink${i}">${esc(it.run.url || 'Start it')}<span class="ar">↗</span></button><p class="rbody v">${esc(it.run.note || it.run.cmd || 'Starts when you press it.')}</p></div>`
9743
+ ? `<div class="ropen" id="runlocal${i}"><button type="button" class="olink" data-saact="preview:${i}" id="prevlink${i}">${esc(it.run.url || 'Start it')}<span class="ar">↗</span></button><p class="rbody v">${esc(it.run.note || it.run.cmd || 'Starts when you press it.')}${it.interruptedTask ? ' Available now; it does not wait for this run to finish.' : ''}</p></div>`
9737
9744
  : it.pr
9738
9745
  ? `<div class="ropen"><a class="olink" href="${esc(it.pr)}" target="_blank" rel="noopener">${esc(it.pr)}<span class="ar">↗</span></a><p class="rbody v">Review and merge this pull request to finish the item.</p></div>`
9739
9746
  : runDeclErrorBody;
9747
+ // An interrupted worker is not a failed implementation. Keep the recovery
9748
+ // record factual and compact: the last recorded step, files already changed,
9749
+ // whether verification remains, and the exact resume action. These are all
9750
+ // state fields — no Manager-generated diagnosis or task re-planning.
9751
+ const interruptedBody = it.interruptedTask ? (() => {
9752
+ const t = it.interruptedTask;
9753
+ const last = (state.act[t.id] ?? t.activity ?? []).filter(Boolean).at(-1);
9754
+ const files = [...new Set((t.changedFiles ?? []).concat(filePaths))];
9755
+ const tests = it.testResult?.ran
9756
+ ? (it.testResult.ok ? 'Verification completed before the interruption.' : 'Verification remains incomplete or failed.')
9757
+ : 'Verification has not completed yet.';
9758
+ const reason = String(t.result || 'The worker reached its run limit before finishing.').replace(/\s+/g, ' ').slice(0, 240);
9759
+ return `<div class="rinfra"><p class="rbody v"><b>Interrupted — resume available</b><br>${esc(reason)}</p>`
9760
+ + (last ? `<p class="rbody v">Last recorded step: ${esc(String(last).slice(0, 220))}</p>` : '')
9761
+ + (files.length ? `<p class="rbody v">Changed so far: ${esc(files.join(' · '))}</p>` : '')
9762
+ + `<p class="rbody v">${esc(tests)}</p><div class="acts2"><button class="errbtn" data-saact="resume:${i}">Resume</button>${errorButtonsHtml(taskErrorMeta(t, it.goal), { taskId: null, goalId: it.goalId, interrupted: true, agent: t.agent || it.goal?.agent })}</div></div>`;
9763
+ })() : '';
9740
9764
  const prRepairBody = it.goal?.prError
9741
9765
  ? `<div class="prfail"><b>${esc(it.goal.prRepair?.title || 'PR blocked')}</b> <span class="dim">${esc(it.goal.prError)}</span>${it.goal.prRepair?.steps?.length ? `<ul>${it.goal.prRepair.steps.map((s) => `<li>${esc(s)}</li>`).join('')}</ul>` : ''}${it.goal.prRepair?.dirtyFiles?.length ? `<div class="dim">dirty: ${it.goal.prRepair.dirtyFiles.map(esc).join(' · ')}</div>` : ''}<div class="acts2"><button class="errbtn" data-saact="retrypr:${i}">${failureLang() === 'ja' ? 'PR再試行' : 'Retry PR'}</button><button class="errbtn" data-saact="glog:${i}">${failureLang() === 'ja' ? 'ログ確認' : 'View log'}</button></div></div>`
9742
9766
  : '';
@@ -9899,8 +9923,8 @@ function saItemHtml(it, i){
9899
9923
  const attn = !working && ['blocked', 'partial', 'failed', 'interrupted'].includes(gstatus);
9900
9924
  // Match the flagship list's state colours (Masa's Interrupt=amber / Failed=red rule): an
9901
9925
  // interrupted goal is amber+resumable, everything else stopped is a red error.
9902
- const attnInt = gstatus === 'interrupted';
9903
- const attnLabel = gstatus === 'interrupted' ? 'Interrupted' : gstatus === 'blocked' ? 'Blocked' : 'Failed';
9926
+ const attnInt = gstatus === 'interrupted' || Boolean(it.interruptedTask);
9927
+ const attnLabel = attnInt ? 'Interrupted resume available' : gstatus === 'blocked' ? 'Blocked' : 'Failed';
9904
9928
  const rst = working
9905
9929
  ? '<span class="rst working"><span class="rdot"></span>Working on it</span>'
9906
9930
  : attn
@@ -9912,6 +9936,7 @@ function saItemHtml(it, i){
9912
9936
  ${meta}
9913
9937
  ${reqSec}
9914
9938
  ${S('Open it', openBody)}
9939
+ ${S('Run state', interruptedBody)}
9915
9940
  ${S('PR', prRepairBody)}
9916
9941
  ${S('Also carries', carriesBody)}
9917
9942
  ${S('What to check', checkBody)}
@@ -10396,6 +10421,11 @@ $('seeall').addEventListener('click', async (e) => {
10396
10421
  }
10397
10422
  const it = SA.items[i];
10398
10423
  if (!it) return;
10424
+ if (k === 'resume') {
10425
+ const taskId = it.interruptedTask?.id;
10426
+ if (taskId != null) return fsRetryTask(taskId);
10427
+ return;
10428
+ }
10399
10429
  if (k === 'retrypr') return fsRetryPrGoal({ disabled: false, textContent: '', dataset: { fsprretry: String(it.goalId) } });
10400
10430
  if (k === 'glog') return toggleGoalDetail(it.goalId);
10401
10431
  // meta-row toggle (H23): tap the token chip to reveal the technique breakdown.
@@ -10598,7 +10628,7 @@ function renderCtxBar(){
10598
10628
  + `<span class="pt">\u200e${esc(folderShort(r.dir))}</span></button>`)
10599
10629
  .join('');
10600
10630
  const openRow = `<button type="button" class="appopt ctxopen" id="ctxopenfolder" title="Open the OS folder chooser"><svg viewBox="0 0 24 24"><path d="M3 7a2 2 0 012-2h4l2 2h8a2 2 0 012 2v8a2 2 0 01-2 2H5a2 2 0 01-2-2z"/><path d="M12 11v6M9 14h6"/></svg><span class="nm">Open folder…</span></button>`;
10601
- menu.innerHTML = `${opts}${openRow}<div class="ctxpath"><input type="text" id="ctxdirin" placeholder="~/path/to/repo" aria-label="Folder path" spellcheck="false"><button type="button" class="appopt" id="ctxdiruse" style="width:auto">Use</button></div><div class="ctxmsg" id="ctxdirmsg"></div>`;
10631
+ menu.innerHTML = `${opts}${openRow}<div class="ctxmsg" id="ctxdirmsg"></div>`;
10602
10632
  }
10603
10633
  // Workspace mode is fixed to the isolated worktree (clean separate checkout). The
10604
10634
  // direct-workspace escape hatch was removed after two accidents where a stray click
@@ -10626,7 +10656,7 @@ async function setProjectFolder(dir){
10626
10656
  const want = String(dir ?? '').trim();
10627
10657
  const msg = $('ctxdirmsg');
10628
10658
  const say = (t, bad) => { if (msg) { msg.textContent = t; msg.classList.toggle('bad', Boolean(bad)); } };
10629
- if (!want) { say('Type a path, or pick one above.', true); return; }
10659
+ if (!want) { say('Choose a folder.', true); return; }
10630
10660
  say('');
10631
10661
  try {
10632
10662
  const r = await fetch(withKey(`/api/projects/${encodeURIComponent(state.active)}`), {
@@ -10648,14 +10678,14 @@ function isLocalSession(){ return ['localhost', '127.0.0.1'].includes(location.h
10648
10678
  async function openFolderNative(){
10649
10679
  const msg = $('ctxdirmsg');
10650
10680
  const say = (t, bad) => { if (msg) { msg.textContent = t; msg.classList.toggle('bad', Boolean(bad)); } };
10651
- if (!isLocalSession()) { $('ctxdirin')?.focus(); say('Remote session pick a repo above or type a path.'); return; }
10681
+ if (!isLocalSession()) { say('Open Manager locally to choose a folder on this computer.', true); return; }
10652
10682
  say('Opening the folder chooser…');
10653
10683
  try {
10654
10684
  const r = await fetch(withKey('/api/choose-folder'), { method: 'POST' });
10655
10685
  const body = await r.json().catch(() => ({}));
10656
10686
  if (body.path) { setProjectFolder(body.path); return; } // reuse: PUT the project's dir, close, render
10657
10687
  if (body.cancelled) { say(''); return; }
10658
- if (body.unsupported) { say('This engine can’t open a folder dialog pick a repo above or type a path.'); return; }
10688
+ if (body.unsupported) { say('This engine can’t open a folder dialog on this system.', true); return; }
10659
10689
  say(body.error || 'Could not open the folder chooser.', true);
10660
10690
  } catch { say('Could not reach the engine.', true); }
10661
10691
  }
@@ -10671,11 +10701,6 @@ $('ctxdirmenu').addEventListener('click', (e) => {
10671
10701
  if (e.target.closest('#ctxopenfolder')) { openFolderNative(); return; }
10672
10702
  const opt = e.target.closest('[data-ctxdir]');
10673
10703
  if (opt) { setProjectFolder(opt.dataset.ctxdir); return; }
10674
- if (e.target.closest('#ctxdiruse')) setProjectFolder($('ctxdirin')?.value ?? '');
10675
- });
10676
- $('ctxdirmenu').addEventListener('keydown', (e) => {
10677
- e.stopPropagation();
10678
- if (e.key === 'Enter' && !e.isComposing && e.target.id === 'ctxdirin') { e.preventDefault(); setProjectFolder(e.target.value); }
10679
10704
  });
10680
10705
  $('appbtn').onclick = (e) => { e.stopPropagation(); $('appmenu').classList.toggle('show'); };
10681
10706
  $('appmenu').addEventListener('click', (e) => {
package/engine/server.mjs CHANGED
@@ -2593,7 +2593,11 @@ async function startPreview(task) {
2593
2593
  // They never need a child process; treating them like a Manager app made
2594
2594
  // "Open it" fail even though the HTML artifact was ready to view.
2595
2595
  if (!cmd && typeof url === 'string' && url.startsWith('/upload/')) {
2596
- return { url, reused: false, started: false };
2596
+ // The card opens this relative URL in a new tab. Carry the manager key in
2597
+ // that URL; otherwise the new navigation loses the authenticated fetch
2598
+ // context and the upload route correctly returns its 403 gate.
2599
+ const keyedUrl = `${url}${url.includes('?') ? '&' : '?'}key=${encodeURIComponent(ACCESS_KEY)}`;
2600
+ return { url: keyedUrl, reused: false, started: false };
2597
2601
  }
2598
2602
  const live = previews.get(task.id);
2599
2603
  if (live && live.proc.exitCode === null) {
@@ -4097,13 +4101,17 @@ const server = createServer(async (req, res) => {
4097
4101
  // (otherwise the dialog would open on the server, not the person's screen). It
4098
4102
  // returns only the chosen path — nothing is scanned, listed, or invented.
4099
4103
  // { path, label } | { cancelled: true } | { unsupported: true } | { error }
4100
- // MANAGER_NO_NATIVE_DIALOG (and any non-macOS host) short-circuits to
4101
- // {unsupported} so tests/CI/headless never block on a GUI.
4104
+ // MANAGER_NO_NATIVE_DIALOG short-circuits so tests/CI/headless never block
4105
+ // on a GUI. macOS, Windows and common Linux desktops use their native picker.
4102
4106
  if (url.pathname === '/api/choose-folder' && req.method === 'POST') {
4103
- if (process.platform !== 'darwin' || process.env.MANAGER_NO_NATIVE_DIALOG) {
4107
+ if (process.env.MANAGER_NO_NATIVE_DIALOG) {
4104
4108
  return json(res, 200, { unsupported: true });
4105
4109
  }
4106
- execFile('osascript', ['-e', chooseFolderScript()], { timeout: 120000 }, (err, stdout, stderr) => {
4110
+ const windowsScript = 'Add-Type -AssemblyName System.Windows.Forms; $d = New-Object System.Windows.Forms.FolderBrowserDialog; if ($d.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK) { [Console]::Write($d.SelectedPath) }';
4111
+ const command = process.platform === 'darwin' ? 'osascript' : process.platform === 'win32' ? 'powershell.exe' : 'zenity';
4112
+ const args = process.platform === 'darwin' ? ['-e', chooseFolderScript()] : process.platform === 'win32'
4113
+ ? ['-NoProfile', '-NonInteractive', '-Command', windowsScript] : ['--file-selection', '--directory', '--title=Choose a project folder'];
4114
+ execFile(command, args, { timeout: 120000 }, (err, stdout, stderr) => {
4107
4115
  const verdict = parseChosenFolder({ code: err ? (err.code ?? 1) : 0, stdout, stderr });
4108
4116
  if (verdict.path) verdict.label = folderLabel(verdict.path, HOME);
4109
4117
  json(res, 200, verdict);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@galda/cli",
3
- "version": "0.10.79",
3
+ "version": "0.10.81",
4
4
  "type": "module",
5
5
  "description": "Galda - hand off work to Claude Code or Codex, get proof back. Runs on your existing subscription, no extra API cost.",
6
6
  "scripts": {