@dotdrelle/wiki-manager 0.15.97 → 0.15.98
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 +2 -2
- package/src/agent/graph.js +41 -6
- package/src/agent/graph.test.js +45 -0
- package/src/cli/wiki-manager.js +47 -33
- package/src/commands/slash.js +7 -2
- package/src/core/agentEvents.js +11 -0
- package/src/core/agentEvents.test.js +25 -0
- package/src/core/buildInfo.json +2 -2
- package/src/core/mcp.js +1 -1
- package/src/core/testGate.test.js +33 -0
- package/src/core/toolLoop.js +7 -1
- package/src/runtime/controlClassify.test.js +31 -0
- package/src/runtime/runner.js +13 -4
- package/src/runtime/runner.test.js +20 -0
- package/src/runtime/server.js +17 -3
- package/src/runtime/server.test.js +13 -1
- package/src/shell/openExternal.js +43 -0
- package/src/shell/repl.js +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dotdrelle/wiki-manager",
|
|
3
|
-
"version": "0.15.
|
|
3
|
+
"version": "0.15.98",
|
|
4
4
|
"description": "Agentic shell and orchestration cockpit for llm-wiki workspaces.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
},
|
|
20
20
|
"scripts": {
|
|
21
21
|
"start": "bun ./bin/wiki-manager.js",
|
|
22
|
-
"test": "node --test src/core/skillInvocation.test.js src/core/skillCompiler.test.js src/runtime/skillRun.test.js src/runtime/controlDrain.test.js src/runtime/controlCancellation.test.js src/cli/runtimeStartup.test.js src/cli/wiki-manager.test.js src/agent/graph.test.js src/agent/skillRecursion.test.js src/contracts/schemas.test.js src/core/activity.test.js src/core/env.test.js src/core/agentsCompose.test.js src/core/profileServiceStatus.test.js src/core/workspaceProfile.test.js src/core/buildInfo.test.js src/core/agentEvents.test.js src/core/skillChainView.test.js src/core/runtimeLog.test.js src/core/runtimeEventAdapter.test.js src/activity/activityAggregator.test.js src/graph/runGraphProjector.test.js src/core/workflow.test.js src/core/planPatch.test.js src/core/agentLoop.test.js src/core/plan.test.js src/core/mcp.test.js src/core/toolLoop.test.js src/core/documentIntake.test.js src/core/dockerCompose.test.js src/core/otherWorkspacesRunning.test.js src/core/wikiSetup.test.js src/core/wikiWorkspace.test.js src/core/wikiWorkspaceStart.test.js src/core/wikirc.test.js src/core/workspaceInherit.test.js src/core/cacert.test.js src/core/composeOverrides.test.js src/core/setEnvValue.test.js src/core/commandFailure.test.js src/core/currentArtifact.test.js src/core/googleGrants.test.js src/core/modelFetch.test.js src/core/startupCheck.test.js src/core/queueStore.test.js src/orchestrator/agentRegistry.test.js src/orchestrator/capabilityRegistry.test.js src/orchestrator/capabilityResolver.test.js src/orchestrator/planValidator.test.js src/orchestrator/planIntegrator.test.js src/orchestrator/taskStatuses.test.js src/orchestrator/scheduler.test.js src/orchestrator/attemptManager.test.js src/orchestrator/resultAggregator.test.js src/orchestrator/approvalPolicy.test.js src/orchestrator/dispatcher.test.js src/orchestrator/objectiveResolver.test.js src/orchestrator/providers/fakeRuntimeProvider.test.js src/orchestrator/providers/runtimeProviders.test.js src/orchestrator/providers/dispatcherExternalRuntime.test.js src/orchestrator/providers/deepAgentsProvider.test.js src/commands/slash.test.js src/shell/repl.test.js src/shell/setupWizardModality.test.js src/shell/setupWizardPlaceholders.test.js src/shell/setupWizardSuggestions.test.js src/shell/setupWizardDiscovery.test.js src/shell/wrapText.test.js src/runtime/lifecycle.test.js src/runtime/store.test.js src/runtime/workspaceIsolation.test.js src/runtime/controlMessages.test.js src/runtime/deltaCoalescer.test.js src/runtime/recoveryManager.test.js src/runtime/server.test.js src/runtime/supervisor.test.js src/runtime/delegation.test.js src/runtime/runner.test.js src/runtime/runner.e2e.test.js src/runtime/skillChain.e2e.test.js src/runtime/donna-contract.test.js src/runtime/approvals.test.js src/runtime/auth.test.js src/runtime/totp.test.js src/runtime/loginSession.test.js src/runtime/loginRoutes.test.js",
|
|
22
|
+
"test": "node --test src/core/skillInvocation.test.js src/core/skillCompiler.test.js src/runtime/skillRun.test.js src/runtime/controlDrain.test.js src/runtime/controlCancellation.test.js src/cli/runtimeStartup.test.js src/cli/wiki-manager.test.js src/agent/graph.test.js src/agent/skillRecursion.test.js src/contracts/schemas.test.js src/core/activity.test.js src/core/env.test.js src/core/agentsCompose.test.js src/core/profileServiceStatus.test.js src/core/workspaceProfile.test.js src/core/buildInfo.test.js src/core/testGate.test.js src/core/agentEvents.test.js src/core/skillChainView.test.js src/core/runtimeLog.test.js src/core/runtimeEventAdapter.test.js src/activity/activityAggregator.test.js src/graph/runGraphProjector.test.js src/core/workflow.test.js src/core/planPatch.test.js src/core/agentLoop.test.js src/core/plan.test.js src/core/mcp.test.js src/core/mcpEndpoints.test.js src/core/toolLoop.test.js src/core/documentIntake.test.js src/core/dockerCompose.test.js src/core/otherWorkspacesRunning.test.js src/core/wikiSetup.test.js src/core/wikiWorkspace.test.js src/core/wikiWorkspaceStart.test.js src/core/wikirc.test.js src/core/workspaceInherit.test.js src/core/cacert.test.js src/core/composeOverrides.test.js src/core/setEnvValue.test.js src/core/commandFailure.test.js src/core/currentArtifact.test.js src/core/googleGrants.test.js src/core/modelFetch.test.js src/core/startupCheck.test.js src/core/queueStore.test.js src/orchestrator/agentRegistry.test.js src/orchestrator/capabilityRegistry.test.js src/orchestrator/capabilityResolver.test.js src/orchestrator/planValidator.test.js src/orchestrator/planIntegrator.test.js src/orchestrator/taskStatuses.test.js src/orchestrator/scheduler.test.js src/orchestrator/attemptManager.test.js src/orchestrator/resultAggregator.test.js src/orchestrator/approvalPolicy.test.js src/orchestrator/dispatcher.test.js src/orchestrator/objectiveResolver.test.js src/orchestrator/providers/fakeRuntimeProvider.test.js src/orchestrator/providers/runtimeProviders.test.js src/orchestrator/providers/dispatcherExternalRuntime.test.js src/orchestrator/providers/deepAgentsProvider.test.js src/commands/slash.test.js src/shell/repl.test.js src/shell/setupWizardModality.test.js src/shell/setupWizardPlaceholders.test.js src/shell/setupWizardSuggestions.test.js src/shell/setupWizardDiscovery.test.js src/shell/wrapText.test.js src/runtime/lifecycle.test.js src/runtime/store.test.js src/runtime/workspaceIsolation.test.js src/runtime/controlMessages.test.js src/runtime/controlClassify.test.js src/runtime/deltaCoalescer.test.js src/runtime/recoveryManager.test.js src/runtime/server.test.js src/runtime/supervisor.test.js src/runtime/delegation.test.js src/runtime/runner.test.js src/runtime/runner.e2e.test.js src/runtime/skillChain.e2e.test.js src/runtime/donna-contract.test.js src/runtime/approvals.test.js src/runtime/auth.test.js src/runtime/totp.test.js src/runtime/loginSession.test.js src/runtime/loginRoutes.test.js",
|
|
23
23
|
"check-versions": "node scripts/check-versions.js",
|
|
24
24
|
"prepack": "node scripts/check-versions.js",
|
|
25
25
|
"prepublishOnly": "node scripts/check-versions.js",
|
package/src/agent/graph.js
CHANGED
|
@@ -2031,10 +2031,17 @@ export function createAgentGraph(options = {}) {
|
|
|
2031
2031
|
),
|
|
2032
2032
|
objectives: Number(skillResult.objectiveCount ?? skillResult.objectives ?? 1) || 1,
|
|
2033
2033
|
};
|
|
2034
|
-
} else if (skillResult?.ok === false) {
|
|
2035
|
-
// A recoverable refusal (guessed skill
|
|
2036
|
-
// turn alive so the model can correct itself or
|
|
2037
|
-
// not let the progress note call it a success.
|
|
2034
|
+
} else if (skillResult?.ok === false && skillResult?.needsInput !== true) {
|
|
2035
|
+
// A recoverable refusal (a guessed skill that does not exist):
|
|
2036
|
+
// keep the turn alive so the model can correct itself or
|
|
2037
|
+
// delegate, but do not let the progress note call it a success.
|
|
2038
|
+
//
|
|
2039
|
+
// `needsInput` is excluded on purpose: "ask the user for the
|
|
2040
|
+
// missing scope" is a conversational blocker, and marking it
|
|
2041
|
+
// failed published `runtime__run_skill failed: {…}` into the
|
|
2042
|
+
// progress surfaces while the turn was working exactly as
|
|
2043
|
+
// intended. The `delegate` branch below already treats the
|
|
2044
|
+
// identical case that way; the two must not disagree.
|
|
2038
2045
|
ok = false;
|
|
2039
2046
|
}
|
|
2040
2047
|
}
|
|
@@ -2186,7 +2193,35 @@ export function createAgentGraph(options = {}) {
|
|
|
2186
2193
|
tool_call_id: call.id,
|
|
2187
2194
|
content: boundedResult,
|
|
2188
2195
|
});
|
|
2189
|
-
if (terminalFailure) break;
|
|
2196
|
+
if (terminalFailure || skillLaunch) break;
|
|
2197
|
+
}
|
|
2198
|
+
|
|
2199
|
+
// A skill launch owns execution and ends the turn, so the rest of the
|
|
2200
|
+
// batch is NOT executed — a companion runtime__delegate in the same batch
|
|
2201
|
+
// used to start a second, independent run with nothing in the thread
|
|
2202
|
+
// naming it. Every unexecuted call still gets its tool result: a provider
|
|
2203
|
+
// that sees tool_calls without matching results on a replayed history
|
|
2204
|
+
// rejects the conversation.
|
|
2205
|
+
if (skillLaunch) {
|
|
2206
|
+
const executed = new Set(toolResultMessages.map((message) => message.tool_call_id));
|
|
2207
|
+
const dropped = toolCalls.filter((call) => !executed.has(call.id));
|
|
2208
|
+
const notRun = `Not executed: ${skillLaunch.publicInput} was launched earlier in this `
|
|
2209
|
+
+ 'turn and owns execution from here. Do not start a second run for the same objective.';
|
|
2210
|
+
for (const call of dropped) {
|
|
2211
|
+
emitAgentEvent(state.session, 'tool_call_result', 'tool', {
|
|
2212
|
+
callId: call.id,
|
|
2213
|
+
name: call.function?.name ?? 'tool',
|
|
2214
|
+
ok: false,
|
|
2215
|
+
result: notRun,
|
|
2216
|
+
summary: 'skipped',
|
|
2217
|
+
});
|
|
2218
|
+
toolResultMessages.push({ role: 'tool', tool_call_id: call.id, content: notRun });
|
|
2219
|
+
}
|
|
2220
|
+
if (dropped.length > 0) {
|
|
2221
|
+
state.session._onStep?.(
|
|
2222
|
+
`${dropped.length} tool call(s) skipped: ${skillLaunch.publicInput} owns execution`,
|
|
2223
|
+
);
|
|
2224
|
+
}
|
|
2190
2225
|
}
|
|
2191
2226
|
|
|
2192
2227
|
if (terminalFailure) {
|
|
@@ -2205,7 +2240,7 @@ export function createAgentGraph(options = {}) {
|
|
|
2205
2240
|
// language like the `/turn` skill path. No further model turn: the skill
|
|
2206
2241
|
// is launched and owns execution, so nothing can contradict it.
|
|
2207
2242
|
const response = await generateSkillAcknowledgment(state.session, skillLaunch).catch(
|
|
2208
|
-
() => `Started ${skillLaunch.publicInput} — ${skillLaunch.objectives} step(s)
|
|
2243
|
+
() => `Started ${skillLaunch.publicInput} — ${skillLaunch.objectives} step(s) queued.`,
|
|
2209
2244
|
);
|
|
2210
2245
|
return {
|
|
2211
2246
|
messages: toolResultMessages,
|
package/src/agent/graph.test.js
CHANGED
|
@@ -678,6 +678,14 @@ test('a natural-language skill match cannot drop declared scope and fall back to
|
|
|
678
678
|
const toolResult = session.agentEvents.find((event) => event.type === 'tool_call_result');
|
|
679
679
|
assert.match(toolResult?.payload?.result ?? '', /missingParameters/);
|
|
680
680
|
assert.match(toolResult?.payload?.result ?? '', /never replace a missing parameter with an unscoped/);
|
|
681
|
+
// "Ask the user for the missing scope" is a conversational blocker, not an
|
|
682
|
+
// execution failure — the turn is working exactly as intended. Marking it
|
|
683
|
+
// failed published `runtime__run_skill failed: {…}` into the progress and
|
|
684
|
+
// activity surfaces; the `delegate` branch handles the identical case as a
|
|
685
|
+
// non-failure, and the two must not disagree.
|
|
686
|
+
assert.equal(toolResult?.payload?.ok, true);
|
|
687
|
+
const progress = session.agentEvents.find((event) => event.type === 'assistant_progress');
|
|
688
|
+
assert.doesNotMatch(progress?.payload?.message ?? '', /failed/i);
|
|
681
689
|
} finally {
|
|
682
690
|
rmSync(root, { recursive: true, force: true });
|
|
683
691
|
}
|
|
@@ -776,6 +784,43 @@ test('a recoverable skill refusal lets the delegate fallback run in the same tur
|
|
|
776
784
|
assert.notEqual(result.terminalToolFailure, true);
|
|
777
785
|
});
|
|
778
786
|
|
|
787
|
+
test('an ACCEPTED skill launch stops the rest of the batch, and says what it dropped', async () => {
|
|
788
|
+
// The mirror of the test above: a refusal must let the fallback run, an
|
|
789
|
+
// acceptance must not. The skill owns execution from there, but the loop
|
|
790
|
+
// kept going — a companion runtime__delegate in the same batch started a
|
|
791
|
+
// second, independent run while only the skill was acknowledged, with
|
|
792
|
+
// nothing in the thread naming the delegated one.
|
|
793
|
+
let delegated = false;
|
|
794
|
+
const session = sessionBase({
|
|
795
|
+
runtime: { url: 'http://runtime.test' },
|
|
796
|
+
_runSkillWithinRun: async () => ({ accepted: true, skill: 'pipeline', objectiveCount: 1 }),
|
|
797
|
+
_delegateWithinRun: async () => { delegated = true; return { runId: 'run-2', summary: { tasks: 1 } }; },
|
|
798
|
+
llm: {
|
|
799
|
+
async completeWithTools({ tools }) {
|
|
800
|
+
if (tools.some((tool) => tool.function?.name === 'classify_action_request')) {
|
|
801
|
+
return { content: null, message: { role: 'assistant', content: null }, tool_calls: [{ id: 'classify', type: 'function', function: { name: 'classify_action_request', arguments: '{"action":true}' } }] };
|
|
802
|
+
}
|
|
803
|
+
return {
|
|
804
|
+
content: null, message: { role: 'assistant', content: null },
|
|
805
|
+
tool_calls: [
|
|
806
|
+
{ id: 'skill', type: 'function', function: { name: 'runtime__run_skill', arguments: '{"skillName":"pipeline","selectionKind":"explicit_name"}' } },
|
|
807
|
+
{ id: 'also', type: 'function', function: { name: 'runtime__delegate', arguments: '{"objective":"construire le livrable"}' } },
|
|
808
|
+
],
|
|
809
|
+
};
|
|
810
|
+
},
|
|
811
|
+
},
|
|
812
|
+
});
|
|
813
|
+
await createAgentGraph().invoke({ input: '/pipeline', session });
|
|
814
|
+
assert.equal(delegated, false, 'the delegate must not start a second run behind the skill');
|
|
815
|
+
// …and the skipped call is announced rather than silently dropped, with a
|
|
816
|
+
// tool result so a replayed history is not missing one.
|
|
817
|
+
const skipped = session.agentEvents.find(
|
|
818
|
+
(event) => event.type === 'tool_call_result' && event.payload?.callId === 'also',
|
|
819
|
+
);
|
|
820
|
+
assert.equal(skipped?.payload?.summary, 'skipped');
|
|
821
|
+
assert.match(skipped?.payload?.result ?? '', /owns execution/);
|
|
822
|
+
});
|
|
823
|
+
|
|
779
824
|
test('tool argument normalization repairs only an unambiguous schema-compatible field name', () => {
|
|
780
825
|
const schema = {
|
|
781
826
|
type: 'object',
|
package/src/cli/wiki-manager.js
CHANGED
|
@@ -1725,6 +1725,13 @@ async function runRuntime(argv, agent) {
|
|
|
1725
1725
|
async function executeInteractiveTurn(context, body, { signal, turnId } = {}) {
|
|
1726
1726
|
const input = String(body.input ?? body.prompt ?? '').trim();
|
|
1727
1727
|
if (!input) throw new Error('Missing input.');
|
|
1728
|
+
// The reader's own words, when the caller augmented `input` with system
|
|
1729
|
+
// facts (a status question gets the runtime's fact block appended for the
|
|
1730
|
+
// model). The thread, the SSE stream and the replayed history must all
|
|
1731
|
+
// show what was typed — publishing the fact block as the user's message
|
|
1732
|
+
// put a raw English dump in their bubble and seeded every later turn with
|
|
1733
|
+
// it.
|
|
1734
|
+
const displayInput = String(body.displayInput ?? '').trim() || input;
|
|
1728
1735
|
// The runtime may start while optional agents are still stopped. `/start
|
|
1729
1736
|
// agents` happens in the shell process, so its refreshed MCP snapshot does
|
|
1730
1737
|
// not mutate this long-lived runtime context. Re-probe only while at least
|
|
@@ -1776,7 +1783,7 @@ async function runRuntime(argv, agent) {
|
|
|
1776
1783
|
origin: 'runtime_turn',
|
|
1777
1784
|
turnId,
|
|
1778
1785
|
workspace: context.workspace ?? null,
|
|
1779
|
-
payload: { content:
|
|
1786
|
+
payload: { content: displayInput },
|
|
1780
1787
|
}));
|
|
1781
1788
|
// Read-only chat turn: same chatAccess policy as the Shell UI's /chat, now
|
|
1782
1789
|
// reachable over HTTP so `wiki serve` chat mode gets read tools without
|
|
@@ -1792,39 +1799,46 @@ async function runRuntime(argv, agent) {
|
|
|
1792
1799
|
body.context?.openWikiPages ?? body.context?.openWikiPage,
|
|
1793
1800
|
);
|
|
1794
1801
|
let response;
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1802
|
+
try {
|
|
1803
|
+
if (chatMode) {
|
|
1804
|
+
ephemeral.chatMode = true;
|
|
1805
|
+
ephemeral.chatAccess = readChatAccessConfig();
|
|
1806
|
+
const history = messages.length && messages[messages.length - 1]?.role === 'user'
|
|
1807
|
+
? messages.slice(0, -1)
|
|
1808
|
+
: messages;
|
|
1809
|
+
response = await runHeadlessChatTurn(ephemeral, input, {
|
|
1810
|
+
history,
|
|
1811
|
+
onStep: ephemeral._onStep,
|
|
1812
|
+
// Fragments de réponse publiés au fil de l'eau, coalescés (voir
|
|
1813
|
+
// deltaCoalescer ci-dessus). Le réducteur les agrège dans la dernière
|
|
1814
|
+
// entrée de conversation (`assistant_delta`), que `assistant_message`
|
|
1815
|
+
// vient ensuite figer : les deux interfaces voient la réponse s'écrire
|
|
1816
|
+
// sans qu'un insert SQLite par token ne bloque le flux.
|
|
1817
|
+
onTextDelta: (delta) => deltaCoalescer.push(delta),
|
|
1818
|
+
onTextReset: () => {
|
|
1819
|
+
deltaCoalescer.reset();
|
|
1820
|
+
dispatchAgentEvent(ephemeral, createAgentEvent('assistant_delta_reset', {
|
|
1821
|
+
origin: 'runtime_turn',
|
|
1822
|
+
turnId,
|
|
1823
|
+
workspace: context.workspace ?? null,
|
|
1824
|
+
payload: {},
|
|
1825
|
+
}));
|
|
1826
|
+
},
|
|
1827
|
+
openWikiPages,
|
|
1828
|
+
});
|
|
1829
|
+
} else {
|
|
1830
|
+
ephemeral.openWikiPages = openWikiPages;
|
|
1831
|
+
response = await runAgentTurn(agent, ephemeral, input, { messages, signal });
|
|
1832
|
+
}
|
|
1833
|
+
} finally {
|
|
1834
|
+
// In a `finally`, not after the await: a throwing or aborted turn left
|
|
1835
|
+
// the 80 ms timer armed, so a stray assistant_delta fired AFTER the
|
|
1836
|
+
// "Runtime turn failed" message and appended orphan fragments to the
|
|
1837
|
+
// wrong conversation entry — and the handle kept the session closure
|
|
1838
|
+
// alive. Flush the tail first so ordering survives either way.
|
|
1839
|
+
deltaCoalescer.flush();
|
|
1840
|
+
deltaCoalescer.dispose();
|
|
1824
1841
|
}
|
|
1825
|
-
// Flush the tail before the turn is finalized, then stop the timer.
|
|
1826
|
-
deltaCoalescer.flush();
|
|
1827
|
-
deltaCoalescer.dispose();
|
|
1828
1842
|
// Persist the artifact the turn may have opened/edited (template_write,
|
|
1829
1843
|
// template_read, …) back onto the long-lived session, so the next /turn —
|
|
1830
1844
|
// chat or agent — sees it. The ephemeral session is otherwise discarded.
|
package/src/commands/slash.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import { isTerminal } from '../orchestrator/taskStatuses.js';
|
|
11
11
|
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
|
|
12
|
-
import { openExternalUrl } from '../shell/openExternal.js';
|
|
12
|
+
import { openAppWindowUrl, openExternalUrl } from '../shell/openExternal.js';
|
|
13
13
|
import { classifyCommandFailure, failureHint, rawFailureText } from '../core/commandFailure.js';
|
|
14
14
|
import { basename, join, relative } from 'node:path';
|
|
15
15
|
import { composeServices, listServices, otherWorkspacesRunning, runWikiCli, serviceLogs, serviceNames, serviceStates, startService, stopService } from '../core/compose.js';
|
|
@@ -845,7 +845,7 @@ ${helpPair('/upload <path>', 'Upload document', '/uploads', 'Uploaded docs')}
|
|
|
845
845
|
${helpPair('/upload convert pending', 'Convert pending', '/uploads clean', 'Clean uploads')}
|
|
846
846
|
${helpPair('/wiki', 'Run wiki index', '/wiki run <args>', 'Raw wiki CLI')}
|
|
847
847
|
${helpPair('/chat', 'Chat mode', '/agent [question]', 'Agent mode / one-shot')}
|
|
848
|
-
${helpPair('/openui', 'Open web UI
|
|
848
|
+
${helpPair('/openui', 'Open web UI as a desktop window', '', '')}
|
|
849
849
|
${helpPair('/run status', 'Runtime status', '/run kill', 'Kill runtime run(s)')}
|
|
850
850
|
${helpPair('/run capability <id>', 'Deterministic capability run', '/approve', 'Grant pending approval')}
|
|
851
851
|
${helpPair('/cancel', 'Cancel active run', '', '')}
|
|
@@ -1770,6 +1770,11 @@ export async function handleSlashCommand(line, context) {
|
|
|
1770
1770
|
// No readable session (gate off, or a custom state dir): plain URL.
|
|
1771
1771
|
}
|
|
1772
1772
|
const note = context.session.workspaceEnv ? '' : ' (no workspace loaded — using default port)';
|
|
1773
|
+
// App-mode first: a chromeless Chrome/Edge window picks up serve's own
|
|
1774
|
+
// manifest (name, icon, window-controls-overlay) with no prior "Install"
|
|
1775
|
+
// step. Falls back to the plain default-browser tab when neither is
|
|
1776
|
+
// found (Safari/Firefox-only machines).
|
|
1777
|
+
if (openAppWindowUrl(openUrl)) return { output: `Opening web UI: ${url}${note}` };
|
|
1773
1778
|
if (openExternalUrl(openUrl)) return { output: `Opening web UI: ${url}${note}` };
|
|
1774
1779
|
return { output: `Web UI: ${url}${note}` };
|
|
1775
1780
|
}
|
package/src/core/agentEvents.js
CHANGED
|
@@ -654,6 +654,17 @@ function applyEvent(state, event) {
|
|
|
654
654
|
reason: event.payload?.reason ?? null,
|
|
655
655
|
rejectedAt: event.ts,
|
|
656
656
|
});
|
|
657
|
+
// Same latch release as `approval.granted`: a refusal IS a decision. Only
|
|
658
|
+
// `granted` cleared the status, so a rejected approval left the
|
|
659
|
+
// projection reporting `pending_approval` for the rest of the run —
|
|
660
|
+
// both UIs kept asking for a decision the user had already made, while
|
|
661
|
+
// `explainControlState` found no pending approval and answered "run is
|
|
662
|
+
// active". The two surfaces disagreed until some later run_done
|
|
663
|
+
// overwrote it.
|
|
664
|
+
if (state.status === 'pending_approval'
|
|
665
|
+
&& !(state.approvals ?? []).some((approval) => approval.status === 'pending_approval')) {
|
|
666
|
+
state.status = 'running';
|
|
667
|
+
}
|
|
657
668
|
return;
|
|
658
669
|
case 'run_done':
|
|
659
670
|
state.status = 'done';
|
|
@@ -151,6 +151,31 @@ test('reduceAgentEvents: granting the approval puts the run back to running', ()
|
|
|
151
151
|
assert.equal(projection.status, 'running');
|
|
152
152
|
});
|
|
153
153
|
|
|
154
|
+
test('reduceAgentEvents: REJECTING the approval also puts the run back to running', () => {
|
|
155
|
+
// A refusal is a decision. Only `granted` cleared the latch, so a rejected
|
|
156
|
+
// approval left both UIs asking for a decision already made, for the rest of
|
|
157
|
+
// the run.
|
|
158
|
+
const projection = reduceAgentEvents([
|
|
159
|
+
createAgentEvent('run_started', { origin: 'runtime' }),
|
|
160
|
+
createAgentEvent('plan_set', { origin: 'tool', payload: { steps: ['Rebuild the concepts'] } }),
|
|
161
|
+
createAgentEvent('plan_step_updated', { origin: 'runtime', payload: { step: 1, status: 'waiting_approval' } }),
|
|
162
|
+
createAgentEvent('approval.requested', { origin: 'runtime', payload: { id: 'a1', scope: 'task', taskId: 't1' } }),
|
|
163
|
+
createAgentEvent('approval.rejected', { origin: 'runtime', payload: { id: 'a1', scope: 'task', taskId: 't1' } }),
|
|
164
|
+
]);
|
|
165
|
+
assert.equal(projection.status, 'running');
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test('reduceAgentEvents: rejecting one of two approvals keeps the run blocked', () => {
|
|
169
|
+
const projection = reduceAgentEvents([
|
|
170
|
+
createAgentEvent('run_started', { origin: 'runtime' }),
|
|
171
|
+
createAgentEvent('plan_set', { origin: 'tool', payload: { steps: ['Export', 'Build'] } }),
|
|
172
|
+
createAgentEvent('approval.requested', { origin: 'runtime', payload: { id: 'a1', scope: 'task', taskId: 't1' } }),
|
|
173
|
+
createAgentEvent('approval.requested', { origin: 'runtime', payload: { id: 'a2', scope: 'task', taskId: 't2' } }),
|
|
174
|
+
createAgentEvent('approval.rejected', { origin: 'runtime', payload: { id: 'a1', scope: 'task', taskId: 't1' } }),
|
|
175
|
+
]);
|
|
176
|
+
assert.equal(projection.status, 'pending_approval');
|
|
177
|
+
});
|
|
178
|
+
|
|
154
179
|
test('reduceAgentEvents: an approval request does not hide a genuinely running task', () => {
|
|
155
180
|
const projection = reduceAgentEvents([
|
|
156
181
|
createAgentEvent('run_started', { origin: 'runtime' }),
|
package/src/core/buildInfo.json
CHANGED
package/src/core/mcp.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from 'node:fs';
|
|
2
2
|
import { managerEnvFile, managerMcpEndpointsFile, readEnvFile } from './env.js';
|
|
3
3
|
|
|
4
|
-
const WIKI_MANAGER_VERSION = '0.15.
|
|
4
|
+
const WIKI_MANAGER_VERSION = '0.15.98';
|
|
5
5
|
|
|
6
6
|
function envValue(key) {
|
|
7
7
|
const filePath = managerEnvFile();
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { test } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { execFileSync } from 'node:child_process';
|
|
4
|
+
import { readFileSync } from 'node:fs';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
|
|
8
|
+
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..');
|
|
9
|
+
|
|
10
|
+
// The `npm test` argument list is hand-maintained, and it has drifted twice:
|
|
11
|
+
// four files were once found outside the gate, and mcpEndpoints.test.js was
|
|
12
|
+
// green on disk while nothing ran it. A test nobody runs is worse than no
|
|
13
|
+
// test — it reads as coverage. This is the check that says so.
|
|
14
|
+
test('every test file on disk is in the npm test gate', () => {
|
|
15
|
+
const pkg = JSON.parse(readFileSync(path.join(repoRoot, 'package.json'), 'utf8'));
|
|
16
|
+
const listed = new Set(
|
|
17
|
+
pkg.scripts.test.split(/\s+/).filter((argument) => argument.endsWith('.test.js')),
|
|
18
|
+
);
|
|
19
|
+
const onDisk = execFileSync('find', ['src', '-name', '*.test.js'], {
|
|
20
|
+
cwd: repoRoot,
|
|
21
|
+
encoding: 'utf8',
|
|
22
|
+
})
|
|
23
|
+
.trim()
|
|
24
|
+
.split('\n')
|
|
25
|
+
.filter(Boolean)
|
|
26
|
+
.sort();
|
|
27
|
+
|
|
28
|
+
const missing = onDisk.filter((file) => !listed.has(file));
|
|
29
|
+
assert.deepEqual(missing, [], `test files not run by \`npm test\`:\n${missing.join('\n')}`);
|
|
30
|
+
|
|
31
|
+
const stale = [...listed].filter((file) => !onDisk.includes(file));
|
|
32
|
+
assert.deepEqual(stale, [], `\`npm test\` names files that no longer exist:\n${stale.join('\n')}`);
|
|
33
|
+
});
|
package/src/core/toolLoop.js
CHANGED
|
@@ -129,7 +129,13 @@ async function finalAnswerWithoutTools({
|
|
|
129
129
|
});
|
|
130
130
|
if (result?.tool_calls?.length) return '';
|
|
131
131
|
return String(result?.content ?? result?.message?.content ?? '').trim();
|
|
132
|
-
} catch {
|
|
132
|
+
} catch (err) {
|
|
133
|
+
// An abort is the user cancelling, not an empty answer. Swallowing it here
|
|
134
|
+
// made `runBoundedToolLoop` return `{ content: '', capped: true }`, and the
|
|
135
|
+
// caller printed the iteration-limit notice for a turn that was cancelled
|
|
136
|
+
// — the loop's contract is that an abort escapes, and this was the one
|
|
137
|
+
// call that broke it.
|
|
138
|
+
if (err?.name === 'AbortError' || signal?.aborted) throw err;
|
|
133
139
|
return '';
|
|
134
140
|
}
|
|
135
141
|
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { test } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { classifyControlMessage } from './server.js';
|
|
4
|
+
|
|
5
|
+
const running = { running: true };
|
|
6
|
+
|
|
7
|
+
// A bare "yes" answers the runtime's own last prompt. Anything MORE than a
|
|
8
|
+
// bare yes is a request, and the rule used to be prefix-anchored only: every
|
|
9
|
+
// message merely STARTING on a yes was classified `observe` and answered with
|
|
10
|
+
// a status report, shadowing the modify_run and enqueue_run branches below it.
|
|
11
|
+
test('a bare confirmation during a run is a status check', async () => {
|
|
12
|
+
for (const input of ['oui', 'OK', 'vas-y', "d'accord", 'yes.', 'entendu !']) {
|
|
13
|
+
const result = await classifyControlMessage(input, running);
|
|
14
|
+
assert.equal(result.kind, 'observe', `expected observe for ${JSON.stringify(input)}`);
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test('a plan change that merely opens on a yes is still a plan change', async () => {
|
|
19
|
+
const result = await classifyControlMessage(
|
|
20
|
+
'oui, ajoute une étape de polish après le build',
|
|
21
|
+
running,
|
|
22
|
+
);
|
|
23
|
+
assert.equal(result.kind, 'modify_run');
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test('a new task that opens on a yes reaches the model classifier, not the status branch', async () => {
|
|
27
|
+
const result = await classifyControlMessage("vas-y lance l'export", running, {
|
|
28
|
+
llm: { complete: async () => 'action' },
|
|
29
|
+
});
|
|
30
|
+
assert.equal(result.kind, 'enqueue_run');
|
|
31
|
+
});
|
package/src/runtime/runner.js
CHANGED
|
@@ -9,7 +9,7 @@ import { createBudgetManager, BudgetExceededError } from '../orchestrator/budget
|
|
|
9
9
|
import { createDispatcher } from '../orchestrator/dispatcher.js';
|
|
10
10
|
import { approvalCovered, approvalRequestForTask } from '../orchestrator/approvalPolicy.js';
|
|
11
11
|
import { blockedByFailedDependency, tasksAwaitingApproval } from '../orchestrator/dependencyResolver.js';
|
|
12
|
-
import { isFailed, isPending, isSkipped, isSuccessful, isTerminal, isUnknownStatus } from '../orchestrator/taskStatuses.js';
|
|
12
|
+
import { isCancelled, isFailed, isPending, isSkipped, isSuccessful, isTerminal, isUnknownStatus } from '../orchestrator/taskStatuses.js';
|
|
13
13
|
import { assertValidatedFragment } from '../orchestrator/planValidator.js';
|
|
14
14
|
import { createResultAggregator } from '../orchestrator/resultAggregator.js';
|
|
15
15
|
import { describePlanConcurrency, drainActive, startReadyTasks } from '../orchestrator/scheduler.js';
|
|
@@ -308,6 +308,7 @@ export async function announceRunOutcome(session, { runId, ok, signal = null } =
|
|
|
308
308
|
if (plan.length === 0) return;
|
|
309
309
|
let failed = 0;
|
|
310
310
|
let cancelled = 0;
|
|
311
|
+
let skipped = 0;
|
|
311
312
|
let completed = 0;
|
|
312
313
|
let pending = 0;
|
|
313
314
|
let firstError = null;
|
|
@@ -319,8 +320,14 @@ export async function announceRunOutcome(session, { runId, ok, signal = null } =
|
|
|
319
320
|
step?.error?.message ?? step?.error?.code ?? step?.error
|
|
320
321
|
?? step?.result?.error?.message ?? step?.result?.error?.code ?? '',
|
|
321
322
|
).trim() || null;
|
|
322
|
-
} else if (
|
|
323
|
+
} else if (isCancelled(status)) {
|
|
323
324
|
cancelled += 1;
|
|
325
|
+
} else if (isSkipped(status)) {
|
|
326
|
+
// A chain step abandoned because an earlier required one failed. It fell
|
|
327
|
+
// through every bucket, so a 3-step chain that failed at step 1
|
|
328
|
+
// announced "0/3 réussie(s), 1 en erreur" and never mentioned the two
|
|
329
|
+
// steps nobody ran — precisely the silence the announce rule forbids.
|
|
330
|
+
skipped += 1;
|
|
324
331
|
} else if (isSuccessful(status)) {
|
|
325
332
|
completed += 1;
|
|
326
333
|
} else if (isPending(status) || !isTerminal(status)) {
|
|
@@ -333,13 +340,15 @@ export async function announceRunOutcome(session, { runId, ok, signal = null } =
|
|
|
333
340
|
}
|
|
334
341
|
}
|
|
335
342
|
const total = plan.length;
|
|
336
|
-
const finished = ok && failed === 0 && cancelled === 0 &&
|
|
343
|
+
const finished = ok && failed === 0 && cancelled === 0 && skipped === 0
|
|
344
|
+
&& pending === 0 && completed === total;
|
|
337
345
|
const factLine = finished
|
|
338
346
|
? `Plan terminé avec succès — ${completed}/${total} tâche(s) réussie(s).`
|
|
339
347
|
: `Plan non terminé — ${completed}/${total} tâche(s) réussie(s)` +
|
|
340
348
|
`${pending ? `, ${pending} en attente (approbation ou exécution)` : ''}` +
|
|
341
349
|
`${failed ? `, ${failed} en erreur` : ''}` +
|
|
342
|
-
`${cancelled ? `, ${cancelled} annulée(s)` : ''}
|
|
350
|
+
`${cancelled ? `, ${cancelled} annulée(s)` : ''}` +
|
|
351
|
+
`${skipped ? `, ${skipped} abandonnée(s) faute d'une étape précédente` : ''}.` +
|
|
343
352
|
`${firstError ? ` Première erreur : ${firstError}.` : ''}`;
|
|
344
353
|
let content = factLine;
|
|
345
354
|
const llm = session.llm;
|
|
@@ -1306,6 +1306,26 @@ test('announceRunOutcome never calls a plan with pending tasks a success', async
|
|
|
1306
1306
|
assert.doesNotMatch(message.payload.content, /succès/i);
|
|
1307
1307
|
});
|
|
1308
1308
|
|
|
1309
|
+
test('announceRunOutcome names the steps a failed chain abandoned', async () => {
|
|
1310
|
+
// `skipped` fell through every bucket: a 3-step chain failing at step 1
|
|
1311
|
+
// announced "0/3 réussie(s), 1 en erreur" and never mentioned the two steps
|
|
1312
|
+
// nobody ran. "When something is skipped, say so where the panels read."
|
|
1313
|
+
const session = {
|
|
1314
|
+
agentEvents: [],
|
|
1315
|
+
agentProjection: null,
|
|
1316
|
+
headlessPlan: [
|
|
1317
|
+
{ id: 'a', description: 'Sync', status: 'failed' },
|
|
1318
|
+
{ id: 'b', description: 'Ingest', status: 'skipped' },
|
|
1319
|
+
{ id: 'c', description: 'Build', status: 'skipped' },
|
|
1320
|
+
],
|
|
1321
|
+
};
|
|
1322
|
+
await announceRunOutcome(session, { runId: 'run-3', ok: false });
|
|
1323
|
+
const message = session.agentEvents.find((event) => event.type === 'assistant_message');
|
|
1324
|
+
assert.match(message.payload.content, /non terminé/i);
|
|
1325
|
+
assert.match(message.payload.content, /1 en erreur/);
|
|
1326
|
+
assert.match(message.payload.content, /2 abandonnée\(s\)/);
|
|
1327
|
+
});
|
|
1328
|
+
|
|
1309
1329
|
test('announceRunOutcome reports success only when every task finished', async () => {
|
|
1310
1330
|
const session = {
|
|
1311
1331
|
agentEvents: [],
|
package/src/runtime/server.js
CHANGED
|
@@ -508,6 +508,12 @@ export function startRuntimeServer({
|
|
|
508
508
|
sendJson(response, 400, { error: 'Missing input.' });
|
|
509
509
|
return;
|
|
510
510
|
}
|
|
511
|
+
// What the reader actually typed. `input` below may be replaced by a
|
|
512
|
+
// system fact block for the model; the THREAD must still show the
|
|
513
|
+
// reader's own words — the replacement was persisted as the
|
|
514
|
+
// `user_message` and replayed as history on every later turn, which is
|
|
515
|
+
// exactly the "raw facts never enter the thread" rule it broke.
|
|
516
|
+
const displayInput = input;
|
|
511
517
|
// Read-only chat turns intentionally remain available while an agent
|
|
512
518
|
// run is active. Other interactive turns still become control
|
|
513
519
|
// messages so they cannot start a competing agent decision.
|
|
@@ -603,7 +609,12 @@ export function startRuntimeServer({
|
|
|
603
609
|
return result.body;
|
|
604
610
|
}
|
|
605
611
|
}
|
|
606
|
-
return turn(context, {
|
|
612
|
+
return turn(context, {
|
|
613
|
+
...body,
|
|
614
|
+
input,
|
|
615
|
+
displayInput,
|
|
616
|
+
mode: readOnlyChat ? 'chat' : body.mode,
|
|
617
|
+
}, {
|
|
607
618
|
signal: controller.signal,
|
|
608
619
|
turnId,
|
|
609
620
|
});
|
|
@@ -1789,7 +1800,7 @@ function asksForRunStatus(input) {
|
|
|
1789
1800
|
// semantic judgement about the workspace's domain, so it is never a keyword
|
|
1790
1801
|
// list here — it goes to the model, bounded, and falls back to the choice menu
|
|
1791
1802
|
// (`ambiguous`) rather than guessing when no model is available.
|
|
1792
|
-
async function classifyControlMessage(input, status, { forcedIntent = null, llm = null, session = null } = {}) {
|
|
1803
|
+
export async function classifyControlMessage(input, status, { forcedIntent = null, llm = null, session = null } = {}) {
|
|
1793
1804
|
// Caller (the /control message route) already trims and rejects empty input.
|
|
1794
1805
|
const lower = String(input ?? '').toLowerCase();
|
|
1795
1806
|
const intent = forcedIntent ? String(forcedIntent).toLowerCase() : null;
|
|
@@ -1823,7 +1834,10 @@ async function classifyControlMessage(input, status, { forcedIntent = null, llm
|
|
|
1823
1834
|
// active, the only thing the runtime can act on is a status check: treating
|
|
1824
1835
|
// the word as ordinary conversation made the read-only chat fallback lecture
|
|
1825
1836
|
// the user about switching modes instead of answering.
|
|
1826
|
-
|
|
1837
|
+
// Anchored at BOTH ends: "oui" is a confirmation, "oui, ajoute une étape de
|
|
1838
|
+
// polish" is a plan change. Without the end anchor this branch shadowed
|
|
1839
|
+
// modify_run and enqueue_run for every message merely STARTING on a yes.
|
|
1840
|
+
if (status.running && /^\s*(oui|yes|yep|ok|okay|vas[- ]?y|d'accord|daccord|entendu)\s*[.!…]*\s*$/i.test(lower)) {
|
|
1827
1841
|
return { kind: 'observe', confidence: 0.7, reason: 'confirmation_of_runtime_prompt' };
|
|
1828
1842
|
}
|
|
1829
1843
|
if (status.running && /\b(ajoute|add|change|modifie|modify|remplace|replace|retire|remove|skip|ignore|apr[eè]s|before|after|chaque|each|plan|step|t[aâ]che)\b/i.test(lower)) {
|
|
@@ -1747,6 +1747,7 @@ test('POST /turn hands a run status question to Donna with the runtime facts', a
|
|
|
1747
1747
|
};
|
|
1748
1748
|
let turns = 0;
|
|
1749
1749
|
let turnInput = '';
|
|
1750
|
+
let turnDisplayInput = '';
|
|
1750
1751
|
let turnMode = null;
|
|
1751
1752
|
let handle;
|
|
1752
1753
|
try {
|
|
@@ -1755,7 +1756,13 @@ test('POST /turn hands a run status question to Donna with the runtime facts', a
|
|
|
1755
1756
|
store: { dbPath: ':memory:', getState: () => status, listEvents: () => [] },
|
|
1756
1757
|
getContext: async () => context,
|
|
1757
1758
|
run: async () => new Promise(() => {}),
|
|
1758
|
-
turn: async (_context, options) => {
|
|
1759
|
+
turn: async (_context, options) => {
|
|
1760
|
+
turns += 1;
|
|
1761
|
+
turnInput = options.input;
|
|
1762
|
+
turnDisplayInput = options.displayInput;
|
|
1763
|
+
turnMode = options.mode;
|
|
1764
|
+
return { ok: true };
|
|
1765
|
+
},
|
|
1759
1766
|
});
|
|
1760
1767
|
} catch (err) {
|
|
1761
1768
|
if (err?.code === 'EPERM') { t.skip('network listen is not permitted in this sandbox'); return; }
|
|
@@ -1778,6 +1785,11 @@ test('POST /turn hands a run status question to Donna with the runtime facts', a
|
|
|
1778
1785
|
assert.equal(turnMode, 'chat');
|
|
1779
1786
|
assert.match(turnInput, /Build TechSections/);
|
|
1780
1787
|
assert.match(turnInput, /runtime run, not a production job/i);
|
|
1788
|
+
// …and the THREAD still shows what the reader typed. `executeInteractiveTurn`
|
|
1789
|
+
// persists `displayInput` as the user_message; feeding it the fact block
|
|
1790
|
+
// put a raw English dump in the reader's own bubble and replayed it as
|
|
1791
|
+
// history on every later turn.
|
|
1792
|
+
assert.equal(turnDisplayInput, 'donne le status du job en cours');
|
|
1781
1793
|
} finally {
|
|
1782
1794
|
context.currentAbortController?.abort();
|
|
1783
1795
|
await handle.close();
|
|
@@ -51,3 +51,46 @@ export function openExternalUrl(value, { run = execFileSync } = {}) {
|
|
|
51
51
|
}
|
|
52
52
|
return null;
|
|
53
53
|
}
|
|
54
|
+
|
|
55
|
+
// A Chromium `--app=<url>` window is chromeless from the start — no tab
|
|
56
|
+
// strip, no address bar — and, when the origin serves a manifest (llm-wiki's
|
|
57
|
+
// `/manifest.webmanifest`), it picks up the site's name/icon/theme-color and
|
|
58
|
+
// window-controls-overlay exactly like a formally "installed" PWA, with no
|
|
59
|
+
// prior install step required. `/openui`'s whole point is a desktop-feeling
|
|
60
|
+
// window, so it tries this before falling back to `openExternalUrl`'s plain
|
|
61
|
+
// tab. Same candidate/fallback shape as `openerCandidates` above, and the same
|
|
62
|
+
// contract: returns the opened URL, or null when no Chromium browser answered.
|
|
63
|
+
function appModeCandidates() {
|
|
64
|
+
if (process.platform === 'darwin') {
|
|
65
|
+
return [
|
|
66
|
+
['open', ['-na', 'Google Chrome', '--args']],
|
|
67
|
+
['open', ['-na', 'Microsoft Edge', '--args']],
|
|
68
|
+
];
|
|
69
|
+
}
|
|
70
|
+
if (process.platform === 'win32') {
|
|
71
|
+
return [
|
|
72
|
+
['cmd', ['/c', 'start', '', 'chrome']],
|
|
73
|
+
['cmd', ['/c', 'start', '', 'msedge']],
|
|
74
|
+
];
|
|
75
|
+
}
|
|
76
|
+
return [
|
|
77
|
+
['google-chrome', []],
|
|
78
|
+
['chromium', []],
|
|
79
|
+
['chromium-browser', []],
|
|
80
|
+
['microsoft-edge', []],
|
|
81
|
+
];
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function openAppWindowUrl(value, { run = execFileSync } = {}) {
|
|
85
|
+
const url = normalizeExternalUrl(value);
|
|
86
|
+
if (!url) return null;
|
|
87
|
+
for (const [command, args] of appModeCandidates()) {
|
|
88
|
+
try {
|
|
89
|
+
run(command, [...args, `--app=${url}`], { stdio: 'ignore', timeout: 5_000 });
|
|
90
|
+
return url;
|
|
91
|
+
} catch {
|
|
92
|
+
// Missing binary or non-zero exit: try the next candidate.
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return null;
|
|
96
|
+
}
|
package/src/shell/repl.js
CHANGED
|
@@ -94,7 +94,7 @@ const COMMAND_COMPLETION_DESCRIPTIONS = {
|
|
|
94
94
|
'/clear': 'Clear the conversation screen.',
|
|
95
95
|
'/chat': 'Switch free text to direct LLM chat without tools.',
|
|
96
96
|
'/agent': 'Switch to agent mode, or run one agent request with /agent <question>.',
|
|
97
|
-
'/openui': 'Open the workspace web UI
|
|
97
|
+
'/openui': 'Open the workspace web UI as a chromeless desktop window (Chrome/Edge), or a browser tab as fallback.',
|
|
98
98
|
'/run': 'Inspect, cancel, kill runtime runs, or start a capability run.',
|
|
99
99
|
'/approve': 'Approve a pending runtime run or tool.',
|
|
100
100
|
};
|