@xingular/harness 1.3.1-unstable.0 → 1.3.1-unstable.2
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/CHANGELOG.md +29 -1
- package/README.md +16 -12
- package/builtin/skills/plugin-authoring/SKILL.md +34 -0
- package/dist/cli.js +2334 -1715
- package/dist/sdk/agents/profiles.d.ts +13 -4
- package/dist/sdk/agents/profiles.js +59 -7
- package/dist/sdk/annotations/bridge.d.ts +2 -0
- package/dist/sdk/annotations/bridge.js +4 -0
- package/dist/sdk/annotations/capture.d.ts +44 -2
- package/dist/sdk/annotations/capture.js +104 -3
- package/dist/sdk/annotations/delivery.d.ts +24 -17
- package/dist/sdk/annotations/delivery.js +82 -49
- package/dist/sdk/annotations/inspect.js +7 -6
- package/dist/sdk/annotations/pageScript.js +4 -4
- package/dist/sdk/annotations/part.d.ts +0 -2
- package/dist/sdk/annotations/part.js +4 -33
- package/dist/sdk/annotations/queue.d.ts +2 -2
- package/dist/sdk/annotations/queue.js +12 -7
- package/dist/sdk/annotations/serverMethods.js +11 -7
- package/dist/sdk/annotations/store.d.ts +13 -21
- package/dist/sdk/annotations/store.js +47 -37
- package/dist/sdk/commands/list.js +2 -0
- package/dist/sdk/core/links.d.ts +51 -0
- package/dist/sdk/core/links.js +155 -0
- package/dist/sdk/harness.d.ts +5 -0
- package/dist/sdk/harness.js +11 -6
- package/dist/sdk/index.d.ts +1 -0
- package/dist/sdk/index.js +1 -0
- package/dist/sdk/plugins/builtin.js +11 -6
- package/dist/sdk/plugins/builtinRegistry.d.ts +3 -1
- package/dist/sdk/plugins/builtinRegistry.js +9 -3
- package/dist/sdk/plugins/discover.d.ts +7 -1
- package/dist/sdk/plugins/discover.js +41 -2
- package/dist/sdk/plugins/identity.js +1 -1
- package/dist/sdk/plugins/inventory.js +1 -1
- package/dist/sdk/plugins/manifest.d.ts +2 -0
- package/dist/sdk/plugins/manifest.js +21 -4
- package/dist/sdk/plugins/plugins.d.ts +2 -0
- package/dist/sdk/plugins/plugins.js +1 -0
- package/dist/sdk/plugins/proposals.d.ts +17 -0
- package/dist/sdk/plugins/proposals.js +147 -0
- package/dist/sdk/plugins/types.d.ts +1 -1
- package/dist/sdk/plugins/types.js +1 -1
- package/dist/sdk/prompt/compile.d.ts +11 -2
- package/dist/sdk/prompt/compile.js +32 -4
- package/dist/sdk/prompt/standard.d.ts +1 -1
- package/dist/sdk/prompt/standard.js +1 -1
- package/dist/sdk/server/methods.js +10 -0
- package/dist/sdk/sessions/sessionState.d.ts +13 -0
- package/dist/sdk/sessions/sessionState.js +31 -0
- package/dist/sdk/sessions/sessions.d.ts +7 -0
- package/dist/sdk/sessions/sessions.js +45 -5
- package/dist/sdk/sessions/subagentService.js +3 -3
- package/dist/sdk/terminal/opentui/theme.d.ts +2 -0
- package/dist/sdk/terminal/opentui/theme.js +1 -0
- package/dist/sdk/terminal/piExtension.js +71 -2
- package/dist/sdk/terminal/terminals.d.ts +9 -0
- package/dist/sdk/terminal/terminals.js +46 -14
- package/dist/sdk/terminal/tui/annotateCommand.d.ts +5 -1
- package/dist/sdk/terminal/tui/annotateCommand.js +21 -7
- package/dist/sdk/terminal/tui/annotateSend.d.ts +30 -0
- package/dist/sdk/terminal/tui/annotateSend.js +40 -0
- package/dist/sdk/terminal/tui/commandPages.d.ts +1 -1
- package/dist/sdk/terminal/tui/commandPages.js +2 -7
- package/dist/sdk/terminal/tui/commands.js +4 -2
- package/dist/sdk/terminal/tui/dialogs.js +1 -0
- package/dist/sdk/terminal/tui/keybindings.d.ts +1 -1
- package/dist/sdk/terminal/tui/keybindings.js +1 -1
- package/dist/sdk/terminal/tui/pageOverlay.d.ts +3 -0
- package/dist/sdk/terminal/tui/pageOverlay.js +12 -1
- package/dist/sdk/terminal/tui/pages/agentsPage.js +5 -2
- package/dist/sdk/terminal/tui/pages/annotatePage.d.ts +70 -16
- package/dist/sdk/terminal/tui/pages/annotatePage.js +349 -115
- package/dist/sdk/terminal/tui/pages/pluginsPage.d.ts +6 -0
- package/dist/sdk/terminal/tui/pages/pluginsPage.js +99 -1
- package/dist/sdk/terminal/tui/pages/usagePage.d.ts +4 -2
- package/dist/sdk/terminal/tui/pages/usagePage.js +39 -12
- package/dist/sdk/terminal/tui/pluginCommands.d.ts +1 -1
- package/dist/sdk/terminal/tui/pluginCommands.js +13 -3
- package/dist/sdk/tools/defaultSkills.d.ts +4 -0
- package/dist/sdk/tools/defaultSkills.js +20 -0
- package/dist/sdk/tools/definitions.d.ts +3 -1
- package/dist/sdk/tools/definitions.js +17 -1
- package/dist/sdk/tools/dispatch.js +12 -0
- package/dist/sdk/tools/harnessTools.js +1 -1
- package/dist/sdk/tools/sessionName.d.ts +7 -0
- package/dist/sdk/tools/sessionName.js +53 -8
- package/dist/sdk/world/world.d.ts +0 -2
- package/dist/terminal-opentui.js +2019 -1269
- package/dist/terminal-pi.js +106 -7
- package/docs/README.md +3 -0
- package/docs/architecture/02-sessions-and-runs.md +11 -5
- package/docs/architecture/03-variants.md +1 -3
- package/docs/architecture/04-prompt.md +16 -7
- package/docs/architecture/06-plugins-and-capabilities.md +36 -4
- package/docs/architecture/07-tools-and-gate.md +20 -5
- package/docs/architecture/10-server-protocol.md +52 -7
- package/docs/architecture/11-terminal.md +50 -19
- package/docs/architecture/12-storage.md +1 -0
- package/docs/architecture/13-world-tools.md +8 -6
- package/docs/architecture/README.md +3 -1
- package/docs/architecture/proposals/PROP-0018-annotation-queue-and-app-targets.md +6 -2
- package/docs/architecture/proposals/PROP-0048-simple-annotate.md +85 -0
- package/docs/architecture/proposals/README.md +1 -0
- package/docs/decisions/ADR-0044-annotations-pending-or-sent.md +24 -0
- package/docs/decisions/ADR-0045-default-plugin-authoring-skill.md +9 -0
- package/docs/decisions/ADR-0046-hash-bound-plugin-proposals.md +20 -0
- package/docs/decisions/ADR-0047-links-open-with-the-os-handler.md +47 -0
- package/docs/parity.md +7 -4
- package/docs/releases/1.3.1-unstable.1.md +8 -0
- package/docs/releases/1.3.1-unstable.2.md +11 -0
- package/docs/releases/README.md +7 -4
- package/docs/terminal-tui.md +62 -23
- package/package.json +1 -1
- package/dist/sdk/terminal/tui/pages/notesPage.d.ts +0 -75
- package/dist/sdk/terminal/tui/pages/notesPage.js +0 -201
|
@@ -18,15 +18,17 @@ import { FIRST_PARTY_PLUGINS, firstPartyExtensions } from "../plugins/firstParty
|
|
|
18
18
|
import { imagePruning } from "../world/images.js";
|
|
19
19
|
import { materialise } from "../variant/materialise.js";
|
|
20
20
|
import { ASK_USER } from "../tools/askUser.js";
|
|
21
|
+
import { defaultSkillPaths } from "../tools/defaultSkills.js";
|
|
21
22
|
import { TODO_WRITE } from "../tools/todo.js";
|
|
22
23
|
import { CHECKPOINT_WRITE, checkpointOf } from "../tools/checkpoint.js";
|
|
23
|
-
import { SESSION_NAME, sessionNameTool } from "../tools/sessionName.js";
|
|
24
|
+
import { SESSION_NAME, deriveSessionName, sessionNameTool } from "../tools/sessionName.js";
|
|
24
25
|
import { DEFAULT_TOOL_NAMES } from "../tools/definitions.js";
|
|
25
26
|
import { GET_CAPABILITIES, getCapabilitiesTool } from "../tools/getCapabilities.js";
|
|
26
27
|
import { isActiveStatus } from "./contracts.js";
|
|
27
28
|
import { SPAWN_SUBAGENT, WAIT_SUBAGENTS, subagentDepth, subagentHistory, subagentNesting } from "./subagents.js";
|
|
28
29
|
import { selectHistory } from "./history.js";
|
|
29
30
|
import { runLineageJoin } from "./lineageJoin.js";
|
|
31
|
+
import { latestSessionName } from "./sessionState.js";
|
|
30
32
|
import { compactionCheckpoint, COMPACTION_CHECKPOINT_EXTENSION } from "./compactionCheckpoint.js";
|
|
31
33
|
import { createSliceState, parseWorkSlice, sliceOf, workSliceGuard, WorkSliceError, WORK_SLICE_EXTENSION } from "./workSlice.js";
|
|
32
34
|
import { piSessionFactory, runTurn, summarize } from "./runner.js";
|
|
@@ -468,6 +470,28 @@ export class Sessions {
|
|
|
468
470
|
updateRun(root, runId, (record) => Object.assign(record, { status: 'failed', endedAt: now(), terminalReason: message }));
|
|
469
471
|
log(`session ${runId} did not start: ${message}`, code);
|
|
470
472
|
};
|
|
473
|
+
// Plugins are resolved once per run: for the profiles the prompt lists, then for what Pi loads.
|
|
474
|
+
const { plugins } = this.options;
|
|
475
|
+
let pluginLoading = null;
|
|
476
|
+
const loadPlugins = plugins
|
|
477
|
+
? () => (pluginLoading ??= plugins.loading({ cwd: input.cwd, agentDir: context.configDir, install: true, caller: { agentId: caller.agentId ?? null, projectId: caller.projectId ?? null }, contributions: capabilities.contributions }))
|
|
478
|
+
: null;
|
|
479
|
+
// A person's main session sees its current name and whether this run's todo is empty. A
|
|
480
|
+
// command, child, side question or unattended run has no such state to show.
|
|
481
|
+
const stateEligible = !input.command
|
|
482
|
+
&& !input.run.relation
|
|
483
|
+
&& (input.depth ?? 0) === 0
|
|
484
|
+
&& !input.automatic
|
|
485
|
+
&& (input.run.trigger == null || input.run.trigger === 'user');
|
|
486
|
+
const existingName = stateEligible ? latestSessionName(historyFile) : null;
|
|
487
|
+
const sessionState = stateEligible
|
|
488
|
+
? {
|
|
489
|
+
name: existingName,
|
|
490
|
+
todo: [],
|
|
491
|
+
...(continuation?.todo.length ? { previousTodo: continuation.todo } : {}),
|
|
492
|
+
firstTurn: !historyFile
|
|
493
|
+
}
|
|
494
|
+
: undefined;
|
|
471
495
|
let system;
|
|
472
496
|
let update;
|
|
473
497
|
try {
|
|
@@ -483,14 +507,17 @@ export class Sessions {
|
|
|
483
507
|
contributions: capabilities.contributions,
|
|
484
508
|
descriptors: BUILTIN_PLUGIN_REGISTRY.descriptors()
|
|
485
509
|
} : undefined;
|
|
510
|
+
if (capabilities.tools.includes(SPAWN_SUBAGENT) && loadPlugins && !input.automatic)
|
|
511
|
+
input.packageProfiles = (await loadPlugins()).profilePaths;
|
|
486
512
|
const profiles = capabilities.tools.includes(SPAWN_SUBAGENT)
|
|
487
|
-
? profileSection(root, input.cwd, variant?.dir ?? null, input.plan === true, researchContext)
|
|
513
|
+
? profileSection(root, input.cwd, variant?.dir ?? null, input.plan === true, researchContext, input.packageProfiles ?? [])
|
|
488
514
|
: null;
|
|
489
515
|
if (profiles && variant)
|
|
490
516
|
variant = { ...variant, sections: variant.sections.filter((section) => section.id !== profiles.id) };
|
|
491
517
|
const own = [plan, resumed, profiles].filter((section) => section !== null);
|
|
492
518
|
const effectiveLaunch = {
|
|
493
519
|
...launch,
|
|
520
|
+
...(sessionState ? { sessionState } : {}),
|
|
494
521
|
sections: own.length ? [...(launch.sections ?? []).filter((section) => !own.some((entry) => entry.id === section.id)), ...own] : launch.sections,
|
|
495
522
|
tools: capabilities.tools,
|
|
496
523
|
connectionTools: (launch.connectionTools ?? []).filter((name) => capabilities.tools.includes(name)),
|
|
@@ -516,7 +543,6 @@ export class Sessions {
|
|
|
516
543
|
// What every session has, after the tools the caller switched on and in a fixed order.
|
|
517
544
|
const allow = capabilities.tools;
|
|
518
545
|
const session = { run, caller, allow, depth, cwd: input.cwd, capabilities };
|
|
519
|
-
const { plugins } = this.options;
|
|
520
546
|
let pi;
|
|
521
547
|
const naming = sessionNameTool(() => {
|
|
522
548
|
const current = pi;
|
|
@@ -598,7 +624,7 @@ export class Sessions {
|
|
|
598
624
|
preamble: input.preamble ?? null,
|
|
599
625
|
automatic: input.automatic,
|
|
600
626
|
account: context,
|
|
601
|
-
resources: { ids: [], skillPaths: variant?.skillPaths ?? [], promptPaths: [] },
|
|
627
|
+
resources: { ids: [], skillPaths: [...(variant?.skillPaths ?? []), ...defaultSkillPaths(variant?.dir)], promptPaths: [] },
|
|
602
628
|
variantExtensions: variant?.extensionPaths ?? [],
|
|
603
629
|
inline: [
|
|
604
630
|
...firstPartyFor(root, capabilities.contributions),
|
|
@@ -608,7 +634,7 @@ export class Sessions {
|
|
|
608
634
|
{ name: COMPACTION_CHECKPOINT_EXTENSION, factory: compactionCheckpoint({ initialTodo: continuation?.todo, planInstructions: checkpointPlanInstructions }) },
|
|
609
635
|
...(sliceEnabled ? [{ name: WORK_SLICE_EXTENSION, factory: workSliceGuard({ policy: slicePolicy, state: sliceState, checkpointAvailable }) }] : [])
|
|
610
636
|
],
|
|
611
|
-
...(plugins ? { plugins: { loading:
|
|
637
|
+
...(plugins && loadPlugins ? { plugins: { loading: loadPlugins, loaded: (extensions) => plugins.loaded(extensions) } } : {}),
|
|
612
638
|
system,
|
|
613
639
|
...(sliceEnabled ? { slice: { state: sliceState, maxToolCalls: slicePolicy.maxToolCalls } } : {}),
|
|
614
640
|
// A command sends no request of its own, so the context update waits for the next turn.
|
|
@@ -635,6 +661,20 @@ export class Sessions {
|
|
|
635
661
|
run.finish('failed', { code: 'native_resume_unavailable', message: 'The saved conversation could not be restored.' });
|
|
636
662
|
throw new HarnessError('native_resume_unavailable', 'The saved conversation could not be restored.');
|
|
637
663
|
}
|
|
664
|
+
// The fallback is a name, not a task step: a refused or failed name never fails the run.
|
|
665
|
+
if (stateEligible && input.autoName !== false && !existingName && !pi.sessionName
|
|
666
|
+
&& pi.setSessionName && capabilities.tools.includes(SESSION_NAME)) {
|
|
667
|
+
const name = deriveSessionName(input.text);
|
|
668
|
+
if (name) {
|
|
669
|
+
try {
|
|
670
|
+
if (await gate.check(SESSION_NAME, { name }) === null)
|
|
671
|
+
pi.setSessionName(name);
|
|
672
|
+
}
|
|
673
|
+
catch {
|
|
674
|
+
// Session naming is metadata; leave the session unnamed.
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
}
|
|
638
678
|
const opened = pi;
|
|
639
679
|
const openedModel = pi.model?.id && pi.model.provider ? `${pi.model.provider}/${pi.model.id}` : null;
|
|
640
680
|
updateRun(root, runId, (record) => {
|
|
@@ -122,7 +122,7 @@ export class Subagents {
|
|
|
122
122
|
if (request.tools !== undefined && (!Array.isArray(request.tools) || request.tools.some(name => typeof name !== 'string'))) {
|
|
123
123
|
return { refusal: 'The optional tools field must be an array of tool names. Omit it to inherit the selected profile’s safe research intersection.' };
|
|
124
124
|
}
|
|
125
|
-
const profile = resolveProfile(this.env.root, parent.input.cwd, request.agent.trim(), parent.input.variant && 'dir' in parent.input.variant ? parent.input.variant.dir : null);
|
|
125
|
+
const profile = resolveProfile(this.env.root, parent.input.cwd, request.agent.trim(), parent.input.variant && 'dir' in parent.input.variant ? parent.input.variant.dir : null, parent.input.packageProfiles);
|
|
126
126
|
if (!profile)
|
|
127
127
|
return { refusal: `There is no agent profile named ${request.agent.trim()}.` };
|
|
128
128
|
const eligibility = planResearchEligibility(profile, {
|
|
@@ -169,7 +169,7 @@ export class Subagents {
|
|
|
169
169
|
if (request.agent !== undefined) {
|
|
170
170
|
if (typeof request.agent !== 'string' || !request.agent.trim())
|
|
171
171
|
throw invalid('agent must be the name of an agent profile.');
|
|
172
|
-
profile = resolveProfile(this.env.root, input.cwd, request.agent.trim(), input.variant && 'dir' in input.variant ? input.variant.dir : null);
|
|
172
|
+
profile = resolveProfile(this.env.root, input.cwd, request.agent.trim(), input.variant && 'dir' in input.variant ? input.variant.dir : null, input.packageProfiles);
|
|
173
173
|
if (!profile)
|
|
174
174
|
throw invalid(`There is no agent profile named ${request.agent.trim()}.`);
|
|
175
175
|
}
|
|
@@ -306,7 +306,7 @@ export class Subagents {
|
|
|
306
306
|
throw invalid(`Subagent ${latest.id} needs capabilities you do not have in this run: ${missing.join(', ')}. Start a new subagent instead.`);
|
|
307
307
|
let profile = null;
|
|
308
308
|
if (latest.profile)
|
|
309
|
-
profile = resolveProfile(root, input.cwd, latest.profile, input.variant && 'dir' in input.variant ? input.variant.dir : null);
|
|
309
|
+
profile = resolveProfile(root, input.cwd, latest.profile, input.variant && 'dir' in input.variant ? input.variant.dir : null, input.packageProfiles);
|
|
310
310
|
if (input.plan) {
|
|
311
311
|
// A plan run continues only a research child whose pinned tools still qualify.
|
|
312
312
|
const check = latest.profile ? this.planSpawnCheck(request.parentRunId, { agent: latest.profile }) : { refusal: 'Plan mode can continue only a stopped research subagent that has a named profile.' };
|
|
@@ -9,14 +9,15 @@ import { controlCall } from "./control.js";
|
|
|
9
9
|
import { SPAWN_SUBAGENT, SUBAGENT_OWNER, SUBAGENT_RECEIPT, SUBAGENT_RESULTS, subagentHistory } from "../sessions/subagents.js";
|
|
10
10
|
import { PLAN_RESEARCH_TOOLS } from "../agents/planResearch.js";
|
|
11
11
|
import { askUserTool } from "../tools/askUser.js";
|
|
12
|
-
import { todoTool } from "../tools/todo.js";
|
|
12
|
+
import { TODO_WRITE, todoTool } from "../tools/todo.js";
|
|
13
13
|
import { CHECKPOINT_WRITE, checkpointTool } from "../tools/checkpoint.js";
|
|
14
14
|
import { createSliceState, parseWorkSlice, workSliceGuard } from "../sessions/workSlice.js";
|
|
15
15
|
import { DEFAULT_COMMAND_TIMEOUT_SECONDS, withCommandBudget } from "../core/commandBudget.js";
|
|
16
|
-
import { sessionNameTool } from "../tools/sessionName.js";
|
|
16
|
+
import { SESSION_NAME, deriveSessionName, sessionNameTool } from "../tools/sessionName.js";
|
|
17
17
|
import { CAPABILITY_DISCOVERY, describeCapabilities } from "../tools/getCapabilities.js";
|
|
18
18
|
import { PLAN_INSTRUCTIONS, planRefusal } from "../sessions/planMode.js";
|
|
19
19
|
import { compactionCheckpoint } from "../sessions/compactionCheckpoint.js";
|
|
20
|
+
import { sessionStateText } from "../prompt/compile.js";
|
|
20
21
|
import { pruneToolImages } from "../world/images.js";
|
|
21
22
|
import { parseInitialBashCommand } from "../sessions/initialBash.js";
|
|
22
23
|
import { inspectWorkspaceWrite } from "../sessions/workspaceBoundary.js";
|
|
@@ -61,6 +62,10 @@ export default async function (pi, planRun, getSession) {
|
|
|
61
62
|
const fileTools = new Map();
|
|
62
63
|
const writeAuthorizations = new Map();
|
|
63
64
|
let workspaceRoot = null;
|
|
65
|
+
let sessionState = null;
|
|
66
|
+
let stateDelivered = false;
|
|
67
|
+
let autoNameTried = false;
|
|
68
|
+
let lastTodo = [];
|
|
64
69
|
// The harness's own tools for this session (connected apps and what the application offers).
|
|
65
70
|
let stage = 'Tool discovery';
|
|
66
71
|
try {
|
|
@@ -139,6 +144,21 @@ export default async function (pi, planRun, getSession) {
|
|
|
139
144
|
pi.registerTool(createWorkspaceShellTool({ createBashTool, createPowerShellTool }, process.cwd(), workspaceBash));
|
|
140
145
|
pi.registerTool(withWorkspaceWriteAuthorization(createWriteTool(process.cwd(), { operations: workspaceWriteOperations }), writeAuthorizations));
|
|
141
146
|
pi.registerTool(withWorkspaceWriteAuthorization(createEditTool(process.cwd(), { operations: workspaceEditOperations }), writeAuthorizations));
|
|
147
|
+
// The current name and this run's todo ride with each request; they never change the stable prefix.
|
|
148
|
+
pi.on('context', (event) => {
|
|
149
|
+
if (!sessionState || stateDelivered)
|
|
150
|
+
return;
|
|
151
|
+
stateDelivered = true;
|
|
152
|
+
return {
|
|
153
|
+
messages: [...event.messages, {
|
|
154
|
+
role: 'custom',
|
|
155
|
+
customType: 'xingular-session-state',
|
|
156
|
+
content: sessionState,
|
|
157
|
+
display: false,
|
|
158
|
+
timestamp: Date.now()
|
|
159
|
+
}]
|
|
160
|
+
};
|
|
161
|
+
});
|
|
142
162
|
compactionCheckpoint({ planInstructions: () => planRun?.active ? PLAN_INSTRUCTIONS : null })(pi);
|
|
143
163
|
// A terminal reply is bounded like any other (PROP-0041): a checkpoint at the boundary, then the
|
|
144
164
|
// person decides whether to continue.
|
|
@@ -154,6 +174,10 @@ export default async function (pi, planRun, getSession) {
|
|
|
154
174
|
});
|
|
155
175
|
pi.on('session_start', async (_event, ctx) => {
|
|
156
176
|
context = ctx;
|
|
177
|
+
sessionState = null;
|
|
178
|
+
stateDelivered = false;
|
|
179
|
+
autoNameTried = false;
|
|
180
|
+
lastTodo = [];
|
|
157
181
|
if (timer)
|
|
158
182
|
clearInterval(timer);
|
|
159
183
|
// The branch's parent runs let the controller show the latest one's rows again on reopen.
|
|
@@ -274,6 +298,44 @@ export default async function (pi, planRun, getSession) {
|
|
|
274
298
|
if (notice)
|
|
275
299
|
return { message: { customType: SUBAGENT_RESULTS, content: notice.text, display: false, details: { [SUBAGENT_RECEIPT]: notice.receipt } } };
|
|
276
300
|
});
|
|
301
|
+
// A new user turn starts with an empty todo. The previous list is context, not this run's list.
|
|
302
|
+
// A substantive first turn also gets a provisional name when the terminal policy allows it.
|
|
303
|
+
pi.on('before_agent_start', async (event, ctx) => {
|
|
304
|
+
if (event.messageType !== 'user')
|
|
305
|
+
return;
|
|
306
|
+
const branch = ctx.sessionManager.getBranch?.() ?? [];
|
|
307
|
+
const hasUser = branch.some((entry) => entry.type === 'message' && entry.message?.role === 'user');
|
|
308
|
+
let name = typeof pi.getSessionName === 'function' ? pi.getSessionName() ?? null : null;
|
|
309
|
+
if (!name && !autoNameTried && pi.setSessionName) {
|
|
310
|
+
const candidate = deriveSessionName(event.prompt);
|
|
311
|
+
if (candidate) {
|
|
312
|
+
autoNameTried = true;
|
|
313
|
+
try {
|
|
314
|
+
const policy = await controlCall('terminal.policy', { tool: SESSION_NAME, args: { name: candidate } });
|
|
315
|
+
if (policy.allowed && !policy.requiresApproval) {
|
|
316
|
+
pi.setSessionName(candidate);
|
|
317
|
+
name = candidate;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
catch {
|
|
321
|
+
// Session naming is metadata; a refused or failed name never fails the turn.
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
const state = {
|
|
326
|
+
name,
|
|
327
|
+
todo: [],
|
|
328
|
+
...(lastTodo.length ? { previousTodo: lastTodo } : {}),
|
|
329
|
+
firstTurn: !hasUser
|
|
330
|
+
};
|
|
331
|
+
lastTodo = [];
|
|
332
|
+
const active = typeof pi.getActiveTools === 'function' ? pi.getActiveTools() : null;
|
|
333
|
+
sessionState = sessionStateText(state, {
|
|
334
|
+
name: active ? active.includes(SESSION_NAME) : true,
|
|
335
|
+
todo: active ? active.includes(TODO_WRITE) : true
|
|
336
|
+
});
|
|
337
|
+
stateDelivered = false;
|
|
338
|
+
});
|
|
277
339
|
const deliverChildren = async (event, ctx, settle) => {
|
|
278
340
|
if (!nativeSubagents || (settle && event.outcome !== 'completed') || ctx.signal?.aborted)
|
|
279
341
|
return;
|
|
@@ -435,6 +497,10 @@ export default async function (pi, planRun, getSession) {
|
|
|
435
497
|
clearInterval(timer);
|
|
436
498
|
timer = null;
|
|
437
499
|
context = null;
|
|
500
|
+
sessionState = null;
|
|
501
|
+
stateDelivered = false;
|
|
502
|
+
autoNameTried = false;
|
|
503
|
+
lastTodo = [];
|
|
438
504
|
// This clears the controller's readiness while the next native session binds. The
|
|
439
505
|
// process-level control claim survives replacement; only the session poller stops.
|
|
440
506
|
await terminalCall('shutdown').catch(() => { });
|
|
@@ -461,6 +527,9 @@ export default async function (pi, planRun, getSession) {
|
|
|
461
527
|
recorded = Promise.resolve();
|
|
462
528
|
const result = await todo.execute(input);
|
|
463
529
|
await recorded;
|
|
530
|
+
const items = input.items;
|
|
531
|
+
if (Array.isArray(items))
|
|
532
|
+
lastTodo = items;
|
|
464
533
|
return { content: [{ type: 'text', text: typeof result === 'string' ? result : result.text }], details: undefined };
|
|
465
534
|
}
|
|
466
535
|
});
|
|
@@ -11,6 +11,7 @@ import type { CallerContext } from "../core/caller.js";
|
|
|
11
11
|
import { type VariantChoice } from "../variant/materialise.js";
|
|
12
12
|
import type { VariantTrust } from "../variant/trust.js";
|
|
13
13
|
import type { AttachParams, PluginScope } from "../plugins/api.js";
|
|
14
|
+
import type { Proposal } from "../plugins/proposals.js";
|
|
14
15
|
import type { Plugins } from "../plugins/plugins.js";
|
|
15
16
|
import type { Plugin } from "../plugins/types.js";
|
|
16
17
|
import type { Hub } from "../hub/hub.js";
|
|
@@ -131,6 +132,12 @@ export interface PluginControl {
|
|
|
131
132
|
remove?(params: PluginScope & {
|
|
132
133
|
pluginId: string;
|
|
133
134
|
}): Promise<Plugin[]>;
|
|
135
|
+
proposals?: {
|
|
136
|
+
list(): Proposal[] | Promise<Proposal[]>;
|
|
137
|
+
preview(id: string): Proposal | Promise<Proposal>;
|
|
138
|
+
accept(id: string, hash: string): Proposal | Promise<Proposal>;
|
|
139
|
+
reject(id: string, hash: string): Proposal | Promise<Proposal>;
|
|
140
|
+
};
|
|
134
141
|
}
|
|
135
142
|
export declare class Terminals {
|
|
136
143
|
private readonly options;
|
|
@@ -138,6 +145,8 @@ export declare class Terminals {
|
|
|
138
145
|
private readonly diagnosticOwners;
|
|
139
146
|
/** The variant each live session runs, for loading its skills. */
|
|
140
147
|
private readonly variantDirs;
|
|
148
|
+
/** The subagent profile files each live session's packages bring, chosen when it started. */
|
|
149
|
+
private readonly packageProfiles;
|
|
141
150
|
/** The world tools each session was given, so a call can only reach one of them. */
|
|
142
151
|
private readonly worldNames;
|
|
143
152
|
private readonly worldReadOnly;
|
|
@@ -17,6 +17,7 @@ import { formatBashCommandForApproval } from "../core/workspaceBashTool.js";
|
|
|
17
17
|
import { resolveWorkspaceRoot } from "../core/workspace.js";
|
|
18
18
|
import { prependSearchPath, searchPath } from "../core/path.js";
|
|
19
19
|
import { dispatch } from "../tools/dispatch.js";
|
|
20
|
+
import { defaultSkillPaths } from "../tools/defaultSkills.js";
|
|
20
21
|
import { materialise } from "../variant/materialise.js";
|
|
21
22
|
import * as componentLibrary from "../variant/components.js";
|
|
22
23
|
import * as variantStore from "../variant/store.js";
|
|
@@ -50,7 +51,7 @@ function resourceIdentity(loading) {
|
|
|
50
51
|
return 'null';
|
|
51
52
|
return JSON.stringify({
|
|
52
53
|
extensions: loading.extensionPaths, builtins: loading.builtins, skills: loading.skillPaths,
|
|
53
|
-
prompts: loading.promptPaths, themes: loading.themePaths, detached: loading.detached,
|
|
54
|
+
prompts: loading.promptPaths, themes: loading.themePaths, profiles: loading.profilePaths, detached: loading.detached,
|
|
54
55
|
project: loading.projectCode, packages: loading.packageCode
|
|
55
56
|
});
|
|
56
57
|
}
|
|
@@ -72,6 +73,8 @@ export class Terminals {
|
|
|
72
73
|
diagnosticOwners = new Map();
|
|
73
74
|
/** The variant each live session runs, for loading its skills. */
|
|
74
75
|
variantDirs = new Map();
|
|
76
|
+
/** The subagent profile files each live session's packages bring, chosen when it started. */
|
|
77
|
+
packageProfiles = new Map();
|
|
75
78
|
/** The world tools each session was given, so a call can only reach one of them. */
|
|
76
79
|
worldNames = new Map();
|
|
77
80
|
worldReadOnly = new Set();
|
|
@@ -358,6 +361,19 @@ export class Terminals {
|
|
|
358
361
|
return params[name];
|
|
359
362
|
};
|
|
360
363
|
switch (call) {
|
|
364
|
+
case 'plugins.proposals.list':
|
|
365
|
+
if (!api.proposals)
|
|
366
|
+
throw invalid('Proposal review is unavailable in this host.');
|
|
367
|
+
return api.proposals.list();
|
|
368
|
+
case 'plugins.proposals.preview':
|
|
369
|
+
if (!api.proposals)
|
|
370
|
+
throw invalid('Proposal review is unavailable in this host.');
|
|
371
|
+
return api.proposals.preview(text('id'));
|
|
372
|
+
case 'plugins.proposals.accept':
|
|
373
|
+
case 'plugins.proposals.reject':
|
|
374
|
+
if (!api.proposals)
|
|
375
|
+
throw invalid('Proposal review is unavailable in this host.');
|
|
376
|
+
return api.proposals[call === 'plugins.proposals.accept' ? 'accept' : 'reject'](text('id'), text('hash'));
|
|
361
377
|
case 'plugins.attach':
|
|
362
378
|
case 'plugins.detach':
|
|
363
379
|
return api.attach({ ...scope, id: text('id'), scope: params.everywhere === true || !projectId ? {} : { projectId } }, call === 'plugins.attach');
|
|
@@ -481,7 +497,7 @@ export class Terminals {
|
|
|
481
497
|
return params.scope;
|
|
482
498
|
};
|
|
483
499
|
switch (call.slice('terminal.agents.'.length)) {
|
|
484
|
-
case 'list': return { ...listProfiles(root, cwd, this.variantDirs.get(live.input.sessionId) ?? null), dirs: profileDirs(root, cwd) };
|
|
500
|
+
case 'list': return { ...listProfiles(root, cwd, this.variantDirs.get(live.input.sessionId) ?? null, this.packageProfiles.get(live.input.sessionId)), dirs: profileDirs(root, cwd) };
|
|
485
501
|
case 'create': return createProfile(root, cwd, { name: name(), scope: scope(), from: typeof params.from === 'string' && params.from ? params.from : null });
|
|
486
502
|
case 'remove':
|
|
487
503
|
removeProfile(root, cwd, { name: name(), scope: scope() });
|
|
@@ -582,6 +598,9 @@ export class Terminals {
|
|
|
582
598
|
if (call === 'terminal.usage.limits') {
|
|
583
599
|
return params.refresh === true ? limits.refresh(live.account.id, { force: true }) : limits.read(live.account.id);
|
|
584
600
|
}
|
|
601
|
+
if (call === 'terminal.usage.allLimits') {
|
|
602
|
+
return params.refresh === true ? limits.refresh() : limits.read();
|
|
603
|
+
}
|
|
585
604
|
if (call === 'terminal.usage.headers') {
|
|
586
605
|
if (typeof params.provider !== 'string' || !params.provider)
|
|
587
606
|
throw invalid('A provider is required.');
|
|
@@ -636,14 +655,14 @@ export class Terminals {
|
|
|
636
655
|
case 'read': return pageNotes.read(root, id());
|
|
637
656
|
case 'create': {
|
|
638
657
|
const note = pageNotes.create(root, { source: params.source, target: params.target, body: params.body, tags: params.tags });
|
|
639
|
-
return params.share === true ?
|
|
658
|
+
return params.share === true ? needNotes().share(note.id, undefined, sessionId) : note;
|
|
640
659
|
}
|
|
641
660
|
case 'update': return pageNotes.update(root, id(), version(), (params.patch ?? {}));
|
|
642
661
|
case 'delete':
|
|
643
662
|
pageNotes.remove(root, id(), version());
|
|
644
663
|
return {};
|
|
645
664
|
// A terminal shares with, and withdraws from, its own session only.
|
|
646
|
-
case 'share': return
|
|
665
|
+
case 'share': return needNotes().share(id(), undefined, sessionId);
|
|
647
666
|
case 'unshare': return pageNotes.unshare(root, id(), { sessionId });
|
|
648
667
|
case 'open': return needCapture().open(sessionId, typeof params.url === 'string' && params.url ? params.url : null);
|
|
649
668
|
case 'identity': return needCapture().identity(sessionId);
|
|
@@ -662,7 +681,7 @@ export class Terminals {
|
|
|
662
681
|
const note = pageNotes.read(root, id());
|
|
663
682
|
return needCapture().reveal(sessionId, note.source.url, note.target);
|
|
664
683
|
}
|
|
665
|
-
// ---- The inspector and
|
|
684
|
+
// ---- The inspector and sending (PROP-0018, PROP-0042); a terminal shares with and sends to its own session.
|
|
666
685
|
case 'target': return { ...needCapture().describe(sessionId), windows: await needCapture().windows(sessionId).catch(() => []) };
|
|
667
686
|
case 'attach': {
|
|
668
687
|
const kind = params.kind;
|
|
@@ -684,10 +703,18 @@ export class Terminals {
|
|
|
684
703
|
await needCapture().stopInspect(sessionId);
|
|
685
704
|
return {};
|
|
686
705
|
case 'next': return inspectStep(needCapture(), needNotes(), sessionId, sessionId, 'terminal');
|
|
687
|
-
case 'queue': return needNotes().queue(id(), version(), sessionId);
|
|
688
|
-
case 'dequeue': return needNotes().dequeue(id(), version());
|
|
689
706
|
case 'confirm': return needNotes().confirm(id(), version());
|
|
690
|
-
case '
|
|
707
|
+
case 'confirmAll': return { notes: needNotes().confirmAll(sessionId) };
|
|
708
|
+
case 'clear': return { notes: needNotes().clear(sessionId) };
|
|
709
|
+
case 'send': return needNotes().send(sessionId, {
|
|
710
|
+
ids: Array.isArray(params.ids) ? params.ids.filter((entry) => typeof entry === 'string') : null,
|
|
711
|
+
prompt: typeof params.prompt === 'string' ? params.prompt : null
|
|
712
|
+
});
|
|
713
|
+
case 'discover': return { apps: await needCapture().discover() };
|
|
714
|
+
case 'launch':
|
|
715
|
+
if (typeof params.path !== 'string' || !params.path.trim())
|
|
716
|
+
throw invalid('Give the path of the app to start.');
|
|
717
|
+
return needCapture().launchApp(sessionId, params.path, Array.isArray(params.args) ? params.args.filter((entry) => typeof entry === 'string') : []);
|
|
691
718
|
default:
|
|
692
719
|
throw new HarnessError('unknown_method', `Unknown call ${call}.`);
|
|
693
720
|
}
|
|
@@ -886,11 +913,12 @@ export class Terminals {
|
|
|
886
913
|
}
|
|
887
914
|
if (call === 'terminal.subagents.tools') {
|
|
888
915
|
const variantDir = this.variantDirs.get(live.input.sessionId) ?? null;
|
|
916
|
+
const packaged = this.packageProfiles.get(live.input.sessionId) ?? [];
|
|
889
917
|
return {
|
|
890
918
|
tools: subagentTools(service, '').map(tool => ({ name: tool.name, label: tool.label, description: tool.description, parameters: tool.parameters(Type) })),
|
|
891
919
|
profiles: {
|
|
892
|
-
normal: profileSection(this.options.env.root, live.input.cwd, variantDir).body,
|
|
893
|
-
plan: profileSection(this.options.env.root, live.input.cwd, variantDir, true).body
|
|
920
|
+
normal: profileSection(this.options.env.root, live.input.cwd, variantDir, false, undefined, packaged).body,
|
|
921
|
+
plan: profileSection(this.options.env.root, live.input.cwd, variantDir, true, undefined, packaged).body
|
|
894
922
|
}
|
|
895
923
|
};
|
|
896
924
|
}
|
|
@@ -912,13 +940,14 @@ export class Terminals {
|
|
|
912
940
|
const caller = { ...live.caller, runId: run.runId };
|
|
913
941
|
const capabilities = this.options.resolveCapabilities ? await this.options.resolveCapabilities(caller, tools, live.input.cwd) : { version: 1, tools, connectors: [], contributions: [] };
|
|
914
942
|
const variantDir = this.variantDirs.get(live.input.sessionId) ?? null;
|
|
943
|
+
const packaged = this.packageProfiles.get(live.input.sessionId) ?? [];
|
|
915
944
|
const subagentProfiles = {
|
|
916
|
-
normal: profileSection(this.options.env.root, live.input.cwd, variantDir).body,
|
|
945
|
+
normal: profileSection(this.options.env.root, live.input.cwd, variantDir, false, undefined, packaged).body,
|
|
917
946
|
plan: profileSection(this.options.env.root, live.input.cwd, variantDir, true, {
|
|
918
947
|
frozenTools: capabilities.tools,
|
|
919
948
|
contributions: capabilities.contributions,
|
|
920
949
|
descriptors: HARNESS_PLUGINS
|
|
921
|
-
}).body
|
|
950
|
+
}, packaged).body
|
|
922
951
|
};
|
|
923
952
|
freezeRun(this.options.env.root, run.runId, caller, capabilities);
|
|
924
953
|
if (!service || live.input.subagents === null)
|
|
@@ -932,6 +961,7 @@ export class Terminals {
|
|
|
932
961
|
connections: live.input.connections === false ? false : { readOnly }, subagents: live.input.subagents,
|
|
933
962
|
plan: params.plan === true,
|
|
934
963
|
variant: live.variant ? { dir: live.variant.dir, scope: live.variant.scope } : null,
|
|
964
|
+
packageProfiles: packaged,
|
|
935
965
|
capabilities
|
|
936
966
|
}, live.account.id, capabilities);
|
|
937
967
|
const history = params.history ?? { owners: [], receipts: [] };
|
|
@@ -1097,11 +1127,12 @@ export class Terminals {
|
|
|
1097
1127
|
plugins: () => this.options.pluginControl?.list({ cwd: input.cwd, accountId: preparedAccount.id, projectId: input.projectId ?? null }) ?? Promise.resolve([]) });
|
|
1098
1128
|
if (variant)
|
|
1099
1129
|
this.variantDirs.set(sessionId, variant.dir);
|
|
1130
|
+
this.packageProfiles.set(sessionId, loading?.profilePaths ?? []);
|
|
1100
1131
|
const worldTools = await this.worldToolNames(input, preparedAccount);
|
|
1101
1132
|
const ownTools = await this.ownToolNames(input, preparedAccount);
|
|
1102
1133
|
const launchTools = [...new Set([...(input.launch?.tools ?? []), TODO_WRITE, SESSION_NAME, ...ownTools, ...(broker ? apps.toolNames : []), ...worldTools])];
|
|
1103
1134
|
// The profiles replace the variant's own subagent list, which they include.
|
|
1104
|
-
const profiles = this.options.subagents && input.subagents !== null ? profileSection(root, input.cwd, variant?.dir ?? null) : null;
|
|
1135
|
+
const profiles = this.options.subagents && input.subagents !== null ? profileSection(root, input.cwd, variant?.dir ?? null, false, undefined, loading?.profilePaths ?? []) : null;
|
|
1105
1136
|
const launchSections = profiles ? [...(input.launch?.sections ?? []).filter((section) => section.id !== profiles.id), profiles] : input.launch?.sections;
|
|
1106
1137
|
// A terminal is a person's own session: the same standard sections a host's session has.
|
|
1107
1138
|
const standard = input.launch?.standard === null ? null : input.launch?.standard ?? { cwd: input.cwd };
|
|
@@ -1280,7 +1311,7 @@ export class Terminals {
|
|
|
1280
1311
|
// The pi command's own extensions are not loaded: a host's session cannot have them (PROP-0039).
|
|
1281
1312
|
for (const path of [...(loading?.extensionPaths ?? []), ...(variant?.extensionPaths ?? [])])
|
|
1282
1313
|
args.push('--extension', path);
|
|
1283
|
-
for (const path of [...(variant?.skillPaths ?? []), ...(loading?.skillPaths ?? [])])
|
|
1314
|
+
for (const path of [...(variant?.skillPaths ?? []), ...defaultSkillPaths(variant?.dir), ...(loading?.skillPaths ?? [])])
|
|
1284
1315
|
args.push('--skill', path);
|
|
1285
1316
|
for (const path of loading?.promptPaths ?? [])
|
|
1286
1317
|
args.push('--prompt-template', path);
|
|
@@ -1311,6 +1342,7 @@ export class Terminals {
|
|
|
1311
1342
|
this.socket.revoke(token);
|
|
1312
1343
|
this.sessions.delete(sessionId);
|
|
1313
1344
|
this.variantDirs.delete(sessionId);
|
|
1345
|
+
this.packageProfiles.delete(sessionId);
|
|
1314
1346
|
this.worldNames.delete(sessionId);
|
|
1315
1347
|
if (this.worktreeLeases.get(sessionId) === acquiredWorktreeId)
|
|
1316
1348
|
this.worktreeLeases.delete(sessionId);
|
|
@@ -1,3 +1,7 @@
|
|
|
1
1
|
import type { AnnotateStart } from "./pages/annotatePage.js";
|
|
2
|
-
/**
|
|
2
|
+
/** A typed page address: a bare host gets https, localhost gets http. */
|
|
3
|
+
export declare function normalizeAddress(text: string): string;
|
|
4
|
+
/** A typed DevTools port or loopback address. */
|
|
5
|
+
export declare function normalizeDevTools(text: string): string;
|
|
6
|
+
/** `/annotate:` accepts a page address, an app's DevTools port, or the page bridge. */
|
|
3
7
|
export declare function parseAnnotate(argument: string): AnnotateStart | string;
|
|
@@ -1,4 +1,20 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/** A typed page address: a bare host gets https, localhost gets http. */
|
|
2
|
+
export function normalizeAddress(text) {
|
|
3
|
+
const word = text.trim();
|
|
4
|
+
if (/^[a-z][a-z0-9+.-]*:\/\//i.test(word))
|
|
5
|
+
return word;
|
|
6
|
+
return /^(localhost|127\.0\.0\.1)(:\d+)?(\/|$)/.test(word) ? `http://${word}` : `https://${word}`;
|
|
7
|
+
}
|
|
8
|
+
/** A typed DevTools port or loopback address. */
|
|
9
|
+
export function normalizeDevTools(text) {
|
|
10
|
+
const word = text.trim();
|
|
11
|
+
if (/^\d+$/.test(word))
|
|
12
|
+
return `http://127.0.0.1:${word}`;
|
|
13
|
+
if (/^(localhost|127\.0\.0\.1|\[::1\]):\d+$/.test(word))
|
|
14
|
+
return `http://${word}`;
|
|
15
|
+
return word;
|
|
16
|
+
}
|
|
17
|
+
/** `/annotate:` accepts a page address, an app's DevTools port, or the page bridge. */
|
|
2
18
|
export function parseAnnotate(argument) {
|
|
3
19
|
const words = argument.trim().split(/\s+/).filter(Boolean);
|
|
4
20
|
if (!words.length)
|
|
@@ -6,17 +22,15 @@ export function parseAnnotate(argument) {
|
|
|
6
22
|
if (words[0] === '--app') {
|
|
7
23
|
const endpoint = words[1];
|
|
8
24
|
if (!endpoint)
|
|
9
|
-
return
|
|
10
|
-
const address = /^\d+$/.test(endpoint) ? `http://127.0.0.1:${endpoint}` : endpoint;
|
|
25
|
+
return { kind: 'apps' };
|
|
11
26
|
const name = words.slice(2).join(' ');
|
|
12
|
-
return { kind: 'cdp', endpoint:
|
|
27
|
+
return { kind: 'cdp', endpoint: normalizeDevTools(endpoint), ...(name ? { name } : {}) };
|
|
13
28
|
}
|
|
14
29
|
if (words[0] === '--bridge') {
|
|
15
30
|
const name = words.slice(1).join(' ');
|
|
16
31
|
return { kind: 'bridge', ...(name ? { name } : {}) };
|
|
17
32
|
}
|
|
18
33
|
if (words.length > 1 || words[0].startsWith('--'))
|
|
19
|
-
return 'Use /annotate:
|
|
20
|
-
|
|
21
|
-
return { kind: 'web', url };
|
|
34
|
+
return 'Use /annotate: [url], /annotate: --app [port], or /annotate: --bridge.';
|
|
35
|
+
return { kind: 'web', url: normalizeAddress(words[0]) };
|
|
22
36
|
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { MenuCall } from "../pluginMenu.js";
|
|
2
|
+
import type { Note } from "../../annotations/store.js";
|
|
3
|
+
type SendResult = {
|
|
4
|
+
state: 'sent';
|
|
5
|
+
ids: string[];
|
|
6
|
+
} | {
|
|
7
|
+
state: 'busy';
|
|
8
|
+
ids: string[];
|
|
9
|
+
} | {
|
|
10
|
+
state: 'host';
|
|
11
|
+
ids: string[];
|
|
12
|
+
} | {
|
|
13
|
+
state: 'empty';
|
|
14
|
+
ids: [];
|
|
15
|
+
unconfirmed?: number;
|
|
16
|
+
};
|
|
17
|
+
export interface AnnotateSendUi {
|
|
18
|
+
confirm(title: string, message: string): Promise<boolean>;
|
|
19
|
+
}
|
|
20
|
+
/** Unsent annotations of `sessionId`, oldest first. */
|
|
21
|
+
export declare function unsent(notes: readonly Note[], sessionId: string | null): Note[];
|
|
22
|
+
/**
|
|
23
|
+
* Sends every unsent annotation of this terminal's session, with `prompt` after them when given.
|
|
24
|
+
* Returns what to tell the person.
|
|
25
|
+
*/
|
|
26
|
+
export declare function sendUnsent(call: MenuCall, ui: AnnotateSendUi, options?: {
|
|
27
|
+
prompt?: string;
|
|
28
|
+
}): Promise<string>;
|
|
29
|
+
export declare function describeSend(result: SendResult, withPrompt?: boolean): string;
|
|
30
|
+
export {};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
const count = (n) => `${n} annotation${n === 1 ? '' : 's'}`;
|
|
2
|
+
/** Unsent annotations of `sessionId`, oldest first. */
|
|
3
|
+
export function unsent(notes, sessionId) {
|
|
4
|
+
return notes
|
|
5
|
+
.filter((note) => note.status === 'open' && note.delivery?.state === 'pending' && note.delivery.sessionId === sessionId)
|
|
6
|
+
.sort((left, right) => left.delivery.queuedAt.localeCompare(right.delivery.queuedAt));
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Sends every unsent annotation of this terminal's session, with `prompt` after them when given.
|
|
10
|
+
* Returns what to tell the person.
|
|
11
|
+
*/
|
|
12
|
+
export async function sendUnsent(call, ui, options = {}) {
|
|
13
|
+
const listed = await call('terminal.annotations.list');
|
|
14
|
+
const waiting = unsent(listed.notes, listed.sessionId);
|
|
15
|
+
if (!waiting.length) {
|
|
16
|
+
return options.prompt
|
|
17
|
+
? 'No unsent annotations. Annotate something with /annotate: first, or send your request as a normal message.'
|
|
18
|
+
: 'Nothing to send. Annotate something first.';
|
|
19
|
+
}
|
|
20
|
+
const unconfirmed = waiting.filter((note) => note.delivery?.confirm);
|
|
21
|
+
if (unconfirmed.length) {
|
|
22
|
+
const yes = await ui.confirm(`Send ${count(waiting.length)} to the agent?`, `${count(unconfirmed.length)} came from the app's own page scripts, which could have written them. Check that the comments are yours.`);
|
|
23
|
+
if (!yes)
|
|
24
|
+
return 'Nothing was sent.';
|
|
25
|
+
await call('terminal.annotations.confirmAll');
|
|
26
|
+
}
|
|
27
|
+
const result = await call('terminal.annotations.send', options.prompt ? { prompt: options.prompt } : {});
|
|
28
|
+
return describeSend(result, !!options.prompt);
|
|
29
|
+
}
|
|
30
|
+
export function describeSend(result, withPrompt = false) {
|
|
31
|
+
if (result.state === 'sent')
|
|
32
|
+
return `Sent ${count(result.ids.length)} to the agent.`;
|
|
33
|
+
if (result.state === 'busy' && withPrompt)
|
|
34
|
+
return 'The agent is still working. Submit again when it finishes; your annotations stay unsent.';
|
|
35
|
+
if (result.state === 'busy')
|
|
36
|
+
return `The agent is working; ${count(result.ids.length)} go${result.ids.length === 1 ? 'es' : ''} out when it finishes.`;
|
|
37
|
+
if (result.state === 'host')
|
|
38
|
+
return `${count(result.ids.length)} handed to the app to send.`;
|
|
39
|
+
return result.unconfirmed ? `${count(result.unconfirmed)} still wait for your confirmation.` : 'Nothing to send.';
|
|
40
|
+
}
|
|
@@ -7,7 +7,7 @@ export declare function createCommandPage(host: TerminalCommandHost, id: Command
|
|
|
7
7
|
runId?: string;
|
|
8
8
|
url?: string;
|
|
9
9
|
annotate?: AnnotateStart;
|
|
10
|
-
usageTab?: 'analysis' | 'limit';
|
|
10
|
+
usageTab?: 'analysis' | 'limit' | 'accounts';
|
|
11
11
|
usagePeriod?: 'today' | '7d' | '30d' | 'all';
|
|
12
12
|
close?(): void;
|
|
13
13
|
}): TerminalPage;
|