@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 +52 -27
- package/engine/server.mjs +13 -5
- package/package.json +1 -1
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
|
-
|
|
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
|
-
|
|
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) {
|
|
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 =
|
|
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="
|
|
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('
|
|
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()) {
|
|
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
|
|
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
|
-
|
|
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
|
|
4101
|
-
//
|
|
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.
|
|
4107
|
+
if (process.env.MANAGER_NO_NATIVE_DIALOG) {
|
|
4104
4108
|
return json(res, 200, { unsupported: true });
|
|
4105
4109
|
}
|
|
4106
|
-
|
|
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