@timqi/pier 0.1.2 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -3
- package/dist/agent/config.js +63 -59
- package/dist/agent/events.js +1 -2
- package/dist/agent/listing.js +38 -15
- package/dist/agent/packages.js +347 -0
- package/dist/agent/pi.js +33 -59
- package/dist/channels/config.js +30 -20
- package/dist/channels/routes.js +0 -3
- package/dist/channels/slack-api.js +19 -10
- package/dist/channels/slack-cli.js +503 -0
- package/dist/channels/slack-directory.js +2 -2
- package/dist/channels/slack-outbound.js +1 -1
- package/dist/channels/slack-thread.js +41 -0
- package/dist/channels/slack-transcript.js +107 -0
- package/dist/channels/slack.js +9 -17
- package/dist/channels/types.js +0 -1
- package/dist/cli.js +158 -7
- package/dist/core/identity.js +20 -11
- package/dist/core/reply.js +16 -15
- package/dist/core/router.js +4 -1
- package/dist/core/types.js +7 -0
- package/dist/db.js +28 -0
- package/dist/extensions/index.js +1 -20
- package/dist/main.js +34 -42
- package/dist/paths.js +3 -0
- package/dist/secrets.js +2 -1
- package/dist/settings.js +5 -0
- package/dist/socket.js +95 -0
- package/dist/tasks/agent.js +5 -6
- package/dist/tasks/callbacks.js +2 -2
- package/dist/tasks/cli.js +215 -0
- package/dist/tasks/definitions.js +3 -3
- package/dist/tasks/execution.js +1 -3
- package/dist/tasks/groups.js +2 -5
- package/dist/tasks/messages.js +18 -158
- package/dist/tasks/operations.js +323 -0
- package/dist/tasks/routes.js +1 -11
- package/dist/tasks/runs.js +5 -20
- package/dist/tasks/service.js +18 -43
- package/dist/tasks/store.js +15 -15
- package/dist/tools.js +63 -85
- package/dist/vault.js +107 -0
- package/dist/web/auth.js +2 -1
- package/dist/web/config.js +25 -11
- package/dist/web/explorer.js +5 -3
- package/dist/web/instance.js +6 -18
- package/dist/web/packages.js +84 -0
- package/dist/web/public/assets/{activity-DOr8dWeX.js → activity-NLS8W9yl.js} +2 -2
- package/dist/web/public/assets/activity-NLS8W9yl.js.br +0 -0
- package/dist/web/public/assets/activity-NLS8W9yl.js.gz +0 -0
- package/dist/web/public/assets/boards-DleegoiC.js +1 -0
- package/dist/web/public/assets/boards-DleegoiC.js.br +0 -0
- package/dist/web/public/assets/boards-DleegoiC.js.gz +0 -0
- package/dist/web/public/assets/explorer-vJvV1Sx0.js +4 -0
- package/dist/web/public/assets/explorer-vJvV1Sx0.js.br +0 -0
- package/dist/web/public/assets/explorer-vJvV1Sx0.js.gz +0 -0
- package/dist/web/public/assets/index-Bs-gol9o.css +2 -0
- package/dist/web/public/assets/index-Bs-gol9o.css.br +0 -0
- package/dist/web/public/assets/index-Bs-gol9o.css.gz +0 -0
- package/dist/web/public/assets/index-Cyp2DKKF.js +85 -0
- package/dist/web/public/assets/index-Cyp2DKKF.js.br +0 -0
- package/dist/web/public/assets/index-Cyp2DKKF.js.gz +0 -0
- package/dist/web/public/assets/runs-C5FveZ75.js +1 -0
- package/dist/web/public/assets/runs-C5FveZ75.js.br +0 -0
- package/dist/web/public/assets/runs-C5FveZ75.js.gz +0 -0
- package/dist/web/public/assets/settings-nJHIBbAz.js +5 -0
- package/dist/web/public/assets/settings-nJHIBbAz.js.br +0 -0
- package/dist/web/public/assets/settings-nJHIBbAz.js.gz +0 -0
- package/dist/web/public/assets/task-runs-CJJ2j5Ks.js +3 -0
- package/dist/web/public/assets/task-runs-CJJ2j5Ks.js.br +0 -0
- package/dist/web/public/assets/task-runs-CJJ2j5Ks.js.gz +0 -0
- package/dist/web/public/assets/tasks-R2Pv0Abg.js +4 -0
- package/dist/web/public/assets/tasks-R2Pv0Abg.js.br +0 -0
- package/dist/web/public/assets/tasks-R2Pv0Abg.js.gz +0 -0
- package/dist/web/public/index.html +2 -2
- package/dist/web/public/index.html.br +0 -0
- package/dist/web/public/index.html.gz +0 -0
- package/dist/web/server.js +3 -1
- package/dist/web/vault.js +68 -0
- package/docs/deploy.md +42 -8
- package/package.json +1 -1
- package/skills/pier-help/SKILL.md +10 -6
- package/skills/pier-slack/SKILL.md +52 -71
- package/skills/pier-tasks/SKILL.md +48 -145
- package/skills/pier-vault/SKILL.md +36 -0
- package/dist/channels/slack-tool.js +0 -416
- package/dist/tasks/tool.js +0 -416
- package/dist/web/public/assets/activity-DOr8dWeX.js.br +0 -0
- package/dist/web/public/assets/activity-DOr8dWeX.js.gz +0 -0
- package/dist/web/public/assets/boards-CneyR23E.js +0 -1
- package/dist/web/public/assets/boards-CneyR23E.js.br +0 -0
- package/dist/web/public/assets/boards-CneyR23E.js.gz +0 -0
- package/dist/web/public/assets/explorer-D0srXT1c.js +0 -4
- package/dist/web/public/assets/explorer-D0srXT1c.js.br +0 -0
- package/dist/web/public/assets/explorer-D0srXT1c.js.gz +0 -0
- package/dist/web/public/assets/index-DbFu15NN.js +0 -85
- package/dist/web/public/assets/index-DbFu15NN.js.br +0 -0
- package/dist/web/public/assets/index-DbFu15NN.js.gz +0 -0
- package/dist/web/public/assets/index-eqQLVS8Q.css +0 -2
- package/dist/web/public/assets/index-eqQLVS8Q.css.br +0 -0
- package/dist/web/public/assets/index-eqQLVS8Q.css.gz +0 -0
- package/dist/web/public/assets/runs-Cv0A-e08.js +0 -1
- package/dist/web/public/assets/runs-Cv0A-e08.js.br +0 -0
- package/dist/web/public/assets/runs-Cv0A-e08.js.gz +0 -0
- package/dist/web/public/assets/settings-VmCjhGBd.js +0 -5
- package/dist/web/public/assets/settings-VmCjhGBd.js.br +0 -0
- package/dist/web/public/assets/settings-VmCjhGBd.js.gz +0 -0
- package/dist/web/public/assets/task-runs-CqpTV644.js +0 -3
- package/dist/web/public/assets/task-runs-CqpTV644.js.br +0 -0
- package/dist/web/public/assets/task-runs-CqpTV644.js.gz +0 -0
- package/dist/web/public/assets/tasks-BffPVgXg.js +0 -4
- package/dist/web/public/assets/tasks-BffPVgXg.js.br +0 -0
- package/dist/web/public/assets/tasks-BffPVgXg.js.gz +0 -0
package/dist/tasks/tool.js
DELETED
|
@@ -1,416 +0,0 @@
|
|
|
1
|
-
import { isAbsolute, resolve } from "node:path";
|
|
2
|
-
import { Type } from "typebox";
|
|
3
|
-
import { logger } from "../log.js";
|
|
4
|
-
import { record, requiredString } from "./definitions.js";
|
|
5
|
-
import { isTerminal } from "./types.js";
|
|
6
|
-
const log = logger("tasks");
|
|
7
|
-
// JSON-Schema enum emits ~1/3 the tokens of typebox's anyOf-of-consts.
|
|
8
|
-
const strEnum = (...values) => Type.Unsafe({ type: "string", enum: [...values] });
|
|
9
|
-
/** Absent instead of `null`: a model reads both the same way, and on a group
|
|
10
|
-
* summary the nulls are a third of the payload. The input names every field
|
|
11
|
-
* so a summary that forgot one cannot pass as "empty". */
|
|
12
|
-
const defined = (value) => Object.fromEntries(Object.entries(value).filter(([, v]) => v !== null && v !== undefined));
|
|
13
|
-
const summarize = (run, pendingDecisionId) => defined({
|
|
14
|
-
runId: run.id,
|
|
15
|
-
taskId: run.taskId,
|
|
16
|
-
taskName: run.context.definition.name,
|
|
17
|
-
state: run.state,
|
|
18
|
-
triggerSource: run.triggerSource,
|
|
19
|
-
groupId: run.groupId,
|
|
20
|
-
sessionMode: run.sessionMode,
|
|
21
|
-
targetSessionId: run.targetSessionId,
|
|
22
|
-
callbackSessionId: run.callbackSessionId,
|
|
23
|
-
callbackState: run.callbackState,
|
|
24
|
-
callbackMode: run.callbackMode ?? null,
|
|
25
|
-
pendingDecisionId,
|
|
26
|
-
depth: run.depth,
|
|
27
|
-
queuedAt: run.queuedAt,
|
|
28
|
-
startedAt: run.startedAt,
|
|
29
|
-
finishedAt: run.finishedAt,
|
|
30
|
-
result: run.result,
|
|
31
|
-
error: run.error,
|
|
32
|
-
skipReason: run.skipReason,
|
|
33
|
-
next: null,
|
|
34
|
-
});
|
|
35
|
-
/** The receipt says the callback is the whole answer, or a model that just
|
|
36
|
-
* launched work reaches for a status call. */
|
|
37
|
-
const receipt = (summary, callbackSessionId, mode, callerSessionId) => ({
|
|
38
|
-
...summary,
|
|
39
|
-
next: callbackSessionId === null
|
|
40
|
-
? "callback none: the result is not delivered to anyone"
|
|
41
|
-
: callbackSessionId !== callerSessionId
|
|
42
|
-
? `the result is delivered to session ${callbackSessionId}; this session will not receive a callback`
|
|
43
|
-
: mode === "steer"
|
|
44
|
-
? "the result interrupts your running turn as a steer message; nothing to query"
|
|
45
|
-
: "the result arrives as a callback message once your turn ends; nothing to query",
|
|
46
|
-
});
|
|
47
|
-
const SUBAGENT_REDIRECT = "subagents cannot redirect callbacks (callback_session_id)";
|
|
48
|
-
/** Who a new run's result goes to — the caller, nobody, or a named session that
|
|
49
|
-
* must exist. Shared by `run` and `resume`: a resumed run is a new run. */
|
|
50
|
-
const callbackTarget = async (input, definitions, callerSessionId) => {
|
|
51
|
-
if (input.callback_session_id === undefined)
|
|
52
|
-
return input.callback === "none" ? null : callerSessionId;
|
|
53
|
-
if (input.callback === "none")
|
|
54
|
-
throw new Error("callback none and callback_session_id conflict: pick one delivery target");
|
|
55
|
-
const target = requiredString(input.callback_session_id, "callback_session_id");
|
|
56
|
-
if (!(await definitions.sessionExists(target)))
|
|
57
|
-
throw new Error(`unknown session: ${target}`);
|
|
58
|
-
return target;
|
|
59
|
-
};
|
|
60
|
-
/** A group echoes many results at once, so each is capped; a single-run
|
|
61
|
-
* `recover` stays whole — it is the escape hatch every truncation note points at. */
|
|
62
|
-
const trimResult = (summary) => {
|
|
63
|
-
if (summary.result?.type !== "agent" || summary.result.text.length <= 2000)
|
|
64
|
-
return summary;
|
|
65
|
-
return {
|
|
66
|
-
...summary,
|
|
67
|
-
result: {
|
|
68
|
-
...summary.result,
|
|
69
|
-
text: `${summary.result.text.slice(0, 2000)}\n[truncated — recover run_id ${summary.runId} with a reason for the full text]`,
|
|
70
|
-
},
|
|
71
|
-
};
|
|
72
|
-
};
|
|
73
|
-
/** Delivered, given up on and reported, or never owed (`callback:"none"`).
|
|
74
|
-
* Anything else is still on its way, and reading it here would be reading it twice. */
|
|
75
|
-
const settled = (callback) => callback.callbackState === null || callback.callbackState === "delivered" || callback.callbackState === "abandoned";
|
|
76
|
-
/** The same words for queued, running, pending and retrying: a refusal that
|
|
77
|
-
* named the state would be the status query this operation replaced. */
|
|
78
|
-
const notRecoverable = (what, callback) => {
|
|
79
|
-
throw new Error(callback.callbackSessionId === null
|
|
80
|
-
? `${what} is not recoverable yet: it was launched with callback none, so nothing will be delivered; recover reads finished results only and cannot wait for work`
|
|
81
|
-
: `${what} is not recoverable yet: wait for automatic delivery to session ${callback.callbackSessionId}; recover cannot wait for work`);
|
|
82
|
-
};
|
|
83
|
-
const summarizeGroup = (group, members, messages) => defined({
|
|
84
|
-
groupId: group.id,
|
|
85
|
-
join: group.join,
|
|
86
|
-
state: group.finishedAt ? "finished" : "running",
|
|
87
|
-
callbackState: group.callbackState,
|
|
88
|
-
callbackMode: group.callbackMode ?? null,
|
|
89
|
-
winnerRunId: group.winnerRunId,
|
|
90
|
-
members: members.map((run) => trimResult(summarize(run, messages.openDecisionId(run.id)))),
|
|
91
|
-
next: null,
|
|
92
|
-
});
|
|
93
|
-
const LaunchSchema = Type.Object({
|
|
94
|
-
model: Type.Optional(Type.Object({ provider: Type.String(), id: Type.String() })),
|
|
95
|
-
thinking: Type.Optional(Type.String()),
|
|
96
|
-
});
|
|
97
|
-
// Guidance only: runtime truth stays in parseDraft, so schema drift cannot
|
|
98
|
-
// loosen boundary validation.
|
|
99
|
-
const DraftSchema = Type.Object({
|
|
100
|
-
name: Type.Optional(Type.String({ description: "Defaults to the prompt's first line." })),
|
|
101
|
-
description: Type.Optional(Type.String()),
|
|
102
|
-
trigger: Type.Optional(Type.Union([
|
|
103
|
-
Type.Object({ type: Type.Literal("manual") }),
|
|
104
|
-
Type.Object({ type: Type.Literal("cron"), expression: Type.String(), timezone: Type.String() }),
|
|
105
|
-
Type.Object({
|
|
106
|
-
type: Type.Literal("watch"),
|
|
107
|
-
script: Type.String(),
|
|
108
|
-
cwd: Type.String(),
|
|
109
|
-
intervalSeconds: Type.Number(),
|
|
110
|
-
mode: strEnum("once", "repeat"),
|
|
111
|
-
}),
|
|
112
|
-
])),
|
|
113
|
-
action: Type.Union([
|
|
114
|
-
Type.Object({
|
|
115
|
-
type: Type.Literal("agent"),
|
|
116
|
-
session: Type.Union([
|
|
117
|
-
Type.Object({ mode: Type.Literal("fresh"), cwd: Type.Optional(Type.String({ description: "Absolute, or relative to your session's directory; omitted = your directory." })) }),
|
|
118
|
-
Type.Object({ mode: Type.Literal("reuse"), sessionId: Type.String() }),
|
|
119
|
-
]),
|
|
120
|
-
prompt: Type.String(),
|
|
121
|
-
launch: Type.Optional(LaunchSchema),
|
|
122
|
-
}),
|
|
123
|
-
Type.Object({ type: Type.Literal("bash"), script: Type.String(), cwd: Type.String() }),
|
|
124
|
-
Type.Object({ type: Type.Literal("task"), taskId: Type.String() }),
|
|
125
|
-
]),
|
|
126
|
-
callback: Type.Optional(Type.Union([
|
|
127
|
-
Type.Object({ type: Type.Literal("none") }),
|
|
128
|
-
Type.Object({ type: Type.Literal("origin") }),
|
|
129
|
-
Type.Object({ type: Type.Literal("session"), sessionId: Type.String() }),
|
|
130
|
-
])),
|
|
131
|
-
timeoutSeconds: Type.Optional(Type.Number()),
|
|
132
|
-
});
|
|
133
|
-
/** The model-facing `task` tool contract, injected into the agent seam as data. */
|
|
134
|
-
export function taskToolSpec(execute) {
|
|
135
|
-
return {
|
|
136
|
-
name: "task",
|
|
137
|
-
label: "Pier Task",
|
|
138
|
-
description: "Manage durable Pier tasks and subagents. Agent tasks run in a fresh session or a reused one. create files a definition the operator sees in the Console — only for schedules or roles you will run again; a one-off is run with a prompt. Run executes a stored task by task_id, a one-shot subagent from a prompt (shorthand: prompt + optional cwd/launch/name/timeoutSeconds — cwd defaults to your own directory, relative paths resolve against it, name comes from the prompt) or from a full inline task draft, or a core-joined fan-out via tasks[] with join all|first. Every operation returns immediately: results, group joins, and decision replies arrive as callback messages once your turn ends — there is no status query; pass callback 'steer' to have a result interrupt your running turn instead, or 'none' for no callback at all. recover (run_id or group_id, plus a reason) re-reads a finished result after its callback has settled — for truncated text or lost context, never to check progress. Use steer/follow_up/resume for child control and contact/reply for supervisor decisions. models lists the deployment's model menu (operator pins with intent notes, else the live catalog).",
|
|
139
|
-
parameters: Type.Object({
|
|
140
|
-
operation: strEnum("list", "create", "update", "run", "recover", "cancel", "steer", "follow_up", "resume", "contact", "reply", "models"),
|
|
141
|
-
task_id: Type.Optional(Type.String()),
|
|
142
|
-
run_id: Type.Optional(Type.String()),
|
|
143
|
-
group_id: Type.Optional(Type.String()),
|
|
144
|
-
message_id: Type.Optional(Type.String()),
|
|
145
|
-
message: Type.Optional(Type.String()),
|
|
146
|
-
reason: Type.Optional(Type.String({ description: "contact: progress | decision. recover: why the delivered callback is not enough (required)." })),
|
|
147
|
-
session_mode: Type.Optional(strEnum("fresh")),
|
|
148
|
-
prompt: Type.Optional(Type.String()),
|
|
149
|
-
cwd: Type.Optional(Type.String()),
|
|
150
|
-
launch: Type.Optional(LaunchSchema),
|
|
151
|
-
name: Type.Optional(Type.String()),
|
|
152
|
-
timeoutSeconds: Type.Optional(Type.Number({ description: "1–86400; defaults to 3600." })),
|
|
153
|
-
task: Type.Optional(DraftSchema),
|
|
154
|
-
// Spelled out, the draft schema costs more tokens per session than the
|
|
155
|
-
// rest of this contract; `parseDraft` validates either shape.
|
|
156
|
-
tasks: Type.Optional(Type.Unsafe({
|
|
157
|
-
type: "array",
|
|
158
|
-
description: "2+ entries, each a prompt string, {prompt, cwd?, launch?, name?, timeoutSeconds?}, a task draft shaped exactly like `task`, or {task_id}.",
|
|
159
|
-
items: { type: "object" },
|
|
160
|
-
})),
|
|
161
|
-
join: Type.Optional(strEnum("all", "first")),
|
|
162
|
-
input: Type.Optional(Type.Unknown()),
|
|
163
|
-
callback: Type.Optional(strEnum("origin", "none", "steer")),
|
|
164
|
-
callback_session_id: Type.Optional(Type.String()),
|
|
165
|
-
}),
|
|
166
|
-
execute,
|
|
167
|
-
};
|
|
168
|
-
}
|
|
169
|
-
export async function handleTaskTool(host, definitions, store, messages, raw, callerSessionId) {
|
|
170
|
-
const input = record(raw);
|
|
171
|
-
if (!input)
|
|
172
|
-
throw new Error("task tool parameters required");
|
|
173
|
-
const active = store.findActiveRunForTarget(callerSessionId);
|
|
174
|
-
if (input.operation === "list")
|
|
175
|
-
return definitions.list().filter((task) => task.kind !== "subagent");
|
|
176
|
-
if (input.operation === "models")
|
|
177
|
-
return host.models();
|
|
178
|
-
if (input.operation === "create") {
|
|
179
|
-
if (active)
|
|
180
|
-
throw new Error("subagents cannot create task definitions");
|
|
181
|
-
return definitions.create(await expandDraft(definitions, input.task, callerSessionId), `session:${callerSessionId}`);
|
|
182
|
-
}
|
|
183
|
-
if (input.operation === "update") {
|
|
184
|
-
if (active)
|
|
185
|
-
throw new Error("subagents cannot update task definitions");
|
|
186
|
-
return definitions.update(requiredString(input.task_id, "task_id"), await expandDraft(definitions, input.task, callerSessionId));
|
|
187
|
-
}
|
|
188
|
-
if (input.operation === "run") {
|
|
189
|
-
const callbackMode = input.callback === "steer" ? "steer" : "followUp";
|
|
190
|
-
// A run's own callback is its parent's link back; a child that could point
|
|
191
|
-
// it elsewhere would strand the supervisor waiting for a result.
|
|
192
|
-
if (active && input.callback_session_id !== undefined)
|
|
193
|
-
throw new Error(SUBAGENT_REDIRECT);
|
|
194
|
-
if (Array.isArray(input.tasks)) {
|
|
195
|
-
// Core-joined fan-out: members run detached, one aggregated callback.
|
|
196
|
-
if (input.task !== undefined || input.task_id !== undefined)
|
|
197
|
-
throw new Error("use either task/task_id or tasks[]");
|
|
198
|
-
if (input.session_mode !== undefined)
|
|
199
|
-
throw new Error("session_mode applies to a single run only");
|
|
200
|
-
if (input.callback_session_id !== undefined)
|
|
201
|
-
throw new Error("callback_session_id applies to a single run only");
|
|
202
|
-
if (input.tasks.length < 2)
|
|
203
|
-
throw new Error("tasks[] needs at least 2 entries; use task for a single run");
|
|
204
|
-
const resolved = [];
|
|
205
|
-
for (const rawEntry of input.tasks) {
|
|
206
|
-
const entry = typeof rawEntry === "string" ? { prompt: rawEntry } : record(rawEntry);
|
|
207
|
-
if (!entry)
|
|
208
|
-
throw new Error("invalid tasks[] entry");
|
|
209
|
-
resolved.push(entry.task_id === undefined
|
|
210
|
-
? await resolveDraft(definitions, entry, active, callerSessionId)
|
|
211
|
-
: resolveStored(definitions, entry.task_id, active));
|
|
212
|
-
}
|
|
213
|
-
const groupCallbackSessionId = input.callback === "none" ? null : callerSessionId;
|
|
214
|
-
const { group, runs } = host.runGroup(resolved, input.join === "first" ? "first" : "all", callerSessionId, active?.id ?? null, groupCallbackSessionId, callbackMode);
|
|
215
|
-
return receipt(summarizeGroup(group, runs, messages), groupCallbackSessionId, callbackMode, callerSessionId);
|
|
216
|
-
}
|
|
217
|
-
const draft = input.task_id === undefined ? inlineDraft(input) : undefined;
|
|
218
|
-
const task = draft
|
|
219
|
-
? await resolveDraft(definitions, draft, active, callerSessionId)
|
|
220
|
-
: resolveStored(definitions, input.task_id, active);
|
|
221
|
-
// Same as the HTTP route: a named mode the schema no longer offers is
|
|
222
|
-
// answered, not quietly swapped for the definition's own policy.
|
|
223
|
-
if (input.session_mode !== undefined && input.session_mode !== "fresh") {
|
|
224
|
-
throw new Error(`unsupported session_mode: ${String(input.session_mode)}`);
|
|
225
|
-
}
|
|
226
|
-
const sessionMode = input.session_mode;
|
|
227
|
-
const callbackSessionId = await callbackTarget(input, definitions, callerSessionId);
|
|
228
|
-
const run = host.run(task.id, input.input, "agent", active?.id ?? null, {
|
|
229
|
-
invokedBySessionId: callerSessionId,
|
|
230
|
-
sourceSessionId: callerSessionId,
|
|
231
|
-
callbackSessionId,
|
|
232
|
-
callbackMode,
|
|
233
|
-
background: true,
|
|
234
|
-
sessionMode,
|
|
235
|
-
});
|
|
236
|
-
return receipt(summarize(run, null), callbackSessionId, callbackMode, callerSessionId);
|
|
237
|
-
}
|
|
238
|
-
if (input.operation === "recover") {
|
|
239
|
-
// History only, never status: readable once the callback has said its last
|
|
240
|
-
// word. The required reason is the friction, and the operator sees it.
|
|
241
|
-
const reason = requiredString(input.reason, "reason");
|
|
242
|
-
// The open question is the notification; the reply's continuation reports.
|
|
243
|
-
const decisionOpen = (run) => {
|
|
244
|
-
throw new Error(`run ${run.id} finished awaiting your decision ${messages.openDecisionId(run.id) ?? ""}; reply to it — the continuation's callback brings the result`);
|
|
245
|
-
};
|
|
246
|
-
// A race winner need not wait for losing members to finish cancelling.
|
|
247
|
-
const groupReady = (group) => {
|
|
248
|
-
if (!group.finishedAt || !settled(group))
|
|
249
|
-
notRecoverable(`group ${group.id}`, group);
|
|
250
|
-
};
|
|
251
|
-
if (typeof input.group_id === "string") {
|
|
252
|
-
const { group, members } = host.getGroup(input.group_id);
|
|
253
|
-
groupReady(group);
|
|
254
|
-
if (!members.every((run) => isTerminal(run.state))) {
|
|
255
|
-
throw new Error(`recover cannot inspect active members; the race has settled — recover its winning result with run_id ${group.winnerRunId ?? "from the callback"} and a reason`);
|
|
256
|
-
}
|
|
257
|
-
const asking = members.find((run) => messages.openDecisionId(run.id));
|
|
258
|
-
if (asking)
|
|
259
|
-
decisionOpen(asking);
|
|
260
|
-
log.info(`recover group ${group.id} by ${callerSessionId}: ${reason}`);
|
|
261
|
-
return summarizeGroup(group, members, messages);
|
|
262
|
-
}
|
|
263
|
-
const run = host.getRun(requiredString(input.run_id, "run_id"));
|
|
264
|
-
if (run.groupId) {
|
|
265
|
-
const { group } = host.getGroup(run.groupId);
|
|
266
|
-
groupReady(group);
|
|
267
|
-
if (!isTerminal(run.state))
|
|
268
|
-
throw new Error("the group has reported its outcome; recover reads finished results only and cannot wait for this member");
|
|
269
|
-
}
|
|
270
|
-
else if (!isTerminal(run.state) || !settled(run)) {
|
|
271
|
-
notRecoverable(`run ${run.id}`, run);
|
|
272
|
-
}
|
|
273
|
-
if (messages.openDecisionId(run.id))
|
|
274
|
-
decisionOpen(run);
|
|
275
|
-
log.info(`recover run ${run.id} by ${callerSessionId}: ${reason}`);
|
|
276
|
-
return summarize(run, null);
|
|
277
|
-
}
|
|
278
|
-
if (input.operation === "cancel") {
|
|
279
|
-
if (typeof input.group_id === "string") {
|
|
280
|
-
const { group, members } = host.getGroup(input.group_id);
|
|
281
|
-
for (const member of members)
|
|
282
|
-
assertOwns(store, callerSessionId, active, member);
|
|
283
|
-
const cancelled = host.cancelGroup(group.id);
|
|
284
|
-
return summarizeGroup(cancelled, cancelled.memberRunIds.map((id) => host.getRun(id)), messages);
|
|
285
|
-
}
|
|
286
|
-
const run = host.getRun(requiredString(input.run_id, "run_id"));
|
|
287
|
-
assertOwns(store, callerSessionId, active, run);
|
|
288
|
-
const cancelled = host.cancel(run.id);
|
|
289
|
-
return summarize(cancelled, messages.openDecisionId(cancelled.id));
|
|
290
|
-
}
|
|
291
|
-
if (input.operation === "steer" || input.operation === "follow_up") {
|
|
292
|
-
const run = host.getRun(requiredString(input.run_id, "run_id"));
|
|
293
|
-
assertOwns(store, callerSessionId, active, run);
|
|
294
|
-
return host.control(run.id, callerSessionId, input.operation, requiredString(input.message, "message"));
|
|
295
|
-
}
|
|
296
|
-
if (input.operation === "resume") {
|
|
297
|
-
const prior = host.getRun(requiredString(input.run_id, "run_id"));
|
|
298
|
-
assertOwns(store, callerSessionId, active, prior);
|
|
299
|
-
// The resumed run is a new run, so it carries its own callback options,
|
|
300
|
-
// under the same rule as `run`: a subagent may not redirect them.
|
|
301
|
-
if (active && input.callback_session_id !== undefined)
|
|
302
|
-
throw new Error(SUBAGENT_REDIRECT);
|
|
303
|
-
const callbackMode = input.callback === "steer" ? "steer" : "followUp";
|
|
304
|
-
const callbackSessionId = await callbackTarget(input, definitions, callerSessionId);
|
|
305
|
-
const run = host.resume(prior.id, requiredString(input.message, "message"), {
|
|
306
|
-
invokedBySessionId: callerSessionId,
|
|
307
|
-
callbackSessionId,
|
|
308
|
-
callbackMode,
|
|
309
|
-
background: true,
|
|
310
|
-
});
|
|
311
|
-
return receipt(summarize(run, null), callbackSessionId, callbackMode, callerSessionId);
|
|
312
|
-
}
|
|
313
|
-
if (input.operation === "contact") {
|
|
314
|
-
if (!active)
|
|
315
|
-
throw new Error("contact is only available inside an active Agent run");
|
|
316
|
-
// The schema no longer narrows `reason` (recover shares the field), so the
|
|
317
|
-
// two names contact accepts are checked here.
|
|
318
|
-
const reason = input.reason === undefined ? "progress" : input.reason;
|
|
319
|
-
if (reason !== "progress" && reason !== "decision")
|
|
320
|
-
throw new Error(`contact reason must be progress or decision, got ${String(reason)}`);
|
|
321
|
-
return messages.contact(active, callerSessionId, reason, requiredString(input.message, "message"));
|
|
322
|
-
}
|
|
323
|
-
if (input.operation === "reply") {
|
|
324
|
-
return messages.reply(requiredString(input.message_id, "message_id"), callerSessionId, requiredString(input.message, "message"));
|
|
325
|
-
}
|
|
326
|
-
throw new Error("unknown task operation");
|
|
327
|
-
}
|
|
328
|
-
/** A single run's draft: the top-level shorthand (`prompt` …) or `task`, never both. */
|
|
329
|
-
function inlineDraft(input) {
|
|
330
|
-
const { prompt, cwd, launch, name, timeoutSeconds } = input;
|
|
331
|
-
if (prompt === undefined)
|
|
332
|
-
return record(input.task) ?? undefined;
|
|
333
|
-
if (input.task !== undefined)
|
|
334
|
-
throw new Error("use either prompt or task");
|
|
335
|
-
return { prompt, cwd, launch, name, timeoutSeconds };
|
|
336
|
-
}
|
|
337
|
-
/** A label for the Console, not an identifier. */
|
|
338
|
-
function nameFromPrompt(prompt) {
|
|
339
|
-
const line = prompt.split("\n")
|
|
340
|
-
.map((l) => l.replace(/^[\s#>*-]+/, "").replace(/[*_`]/g, "").replace(/\s+/g, " ").trim())
|
|
341
|
-
.find(Boolean) ?? "subagent";
|
|
342
|
-
return line.length > 60 ? `${line.slice(0, 59).trimEnd()}…` : line;
|
|
343
|
-
}
|
|
344
|
-
/** A `prompt` shorthand becomes a fresh Agent action in the caller's own
|
|
345
|
-
* directory; everything the caller did spell out passes through to parseDraft. */
|
|
346
|
-
async function expandDraft(definitions, raw, callerSessionId) {
|
|
347
|
-
let draft = record(raw);
|
|
348
|
-
if (!draft)
|
|
349
|
-
return raw;
|
|
350
|
-
if (typeof draft.prompt === "string") {
|
|
351
|
-
if (draft.action !== undefined)
|
|
352
|
-
throw new Error("use either prompt (shorthand) or action");
|
|
353
|
-
const { prompt, cwd, launch, name, ...rest } = draft;
|
|
354
|
-
draft = { ...rest, name, action: { type: "agent", session: { mode: "fresh", cwd }, prompt, launch } };
|
|
355
|
-
}
|
|
356
|
-
const action = record(draft.action);
|
|
357
|
-
const session = record(action?.session);
|
|
358
|
-
if (action?.type === "agent" && session?.mode === "fresh" && (session.cwd === undefined || (typeof session.cwd === "string" && !isAbsolute(session.cwd)))) {
|
|
359
|
-
const base = await definitions.sessionCwd(callerSessionId);
|
|
360
|
-
if (!base)
|
|
361
|
-
throw new Error(`cwd ${session.cwd === undefined ? "omitted" : `"${session.cwd}" is relative`} and the calling session has no working directory; give an absolute path`);
|
|
362
|
-
draft = { ...draft, action: { ...action, session: { ...session, cwd: resolve(base, session.cwd ?? ".") } } };
|
|
363
|
-
}
|
|
364
|
-
if (draft.name === undefined && typeof action?.prompt === "string")
|
|
365
|
-
draft = { ...draft, name: nameFromPrompt(action.prompt) };
|
|
366
|
-
return draft;
|
|
367
|
-
}
|
|
368
|
-
/** Persisted like any task (kind "subagent", filtered from default lists) so
|
|
369
|
-
* runs stay auditable and resumable. */
|
|
370
|
-
async function resolveDraft(definitions, raw, active, callerSessionId) {
|
|
371
|
-
const draft = record(await expandDraft(definitions, raw, callerSessionId));
|
|
372
|
-
if (!draft)
|
|
373
|
-
throw new Error("task definition required");
|
|
374
|
-
if (draft.trigger !== undefined && record(draft.trigger)?.type !== "manual") {
|
|
375
|
-
throw new Error("inline subagent tasks must use a manual trigger");
|
|
376
|
-
}
|
|
377
|
-
// Delivery of a one-off run is the top-level fields' business; a nested
|
|
378
|
-
// callback only means anything on a stored definition's schedule.
|
|
379
|
-
if (draft.callback !== undefined || draft.callback_session_id !== undefined) {
|
|
380
|
-
throw new Error("an inline task draft cannot set callback; use the top-level callback / callback_session_id");
|
|
381
|
-
}
|
|
382
|
-
if (active) {
|
|
383
|
-
const action = record(draft.action);
|
|
384
|
-
if (action?.type !== "agent")
|
|
385
|
-
throw new Error("subagents may only inline Agent tasks");
|
|
386
|
-
if (record(action.session)?.mode === "reuse")
|
|
387
|
-
throw new Error("subagent inline tasks cannot reuse an existing session");
|
|
388
|
-
}
|
|
389
|
-
return definitions.create({ ...draft, trigger: { type: "manual" } }, `session:${callerSessionId}`, "subagent");
|
|
390
|
-
}
|
|
391
|
-
function resolveStored(definitions, taskId, active) {
|
|
392
|
-
const task = definitions.get(requiredString(taskId, "task_id"));
|
|
393
|
-
if (active && task.action.type !== "agent")
|
|
394
|
-
throw new Error("subagents may only invoke Agent tasks");
|
|
395
|
-
return task;
|
|
396
|
-
}
|
|
397
|
-
function assertOwns(store, callerSessionId, active, target) {
|
|
398
|
-
if (active) {
|
|
399
|
-
let cursor = target;
|
|
400
|
-
while (cursor?.parentRunId) {
|
|
401
|
-
if (cursor.parentRunId === active.id)
|
|
402
|
-
return;
|
|
403
|
-
cursor = store.getRun(cursor.parentRunId);
|
|
404
|
-
}
|
|
405
|
-
throw new Error("subagent may only control descendant runs");
|
|
406
|
-
}
|
|
407
|
-
let root = target;
|
|
408
|
-
while (root.parentRunId) {
|
|
409
|
-
const parent = store.getRun(root.parentRunId);
|
|
410
|
-
if (!parent)
|
|
411
|
-
throw new Error(`unknown parent run: ${root.parentRunId}`);
|
|
412
|
-
root = parent;
|
|
413
|
-
}
|
|
414
|
-
if (root.invokedBySessionId !== callerSessionId)
|
|
415
|
-
throw new Error("session does not own this run");
|
|
416
|
-
}
|
|
Binary file
|
|
Binary file
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{E as e,H as t,K as n,N as r,R as i,S as a,U as o,X as s,Y as c,q as l,x as u,y as d,z as f}from"./index-DbFu15NN.js";var p=e=>e.public&&e.token?`/p/${e.slug}-${e.token}/`:`/boards/${e.slug}/`;function m(m,h){let g=[],_=``,v=t(`header`,`pagehead sticky top-[-8px] z-30 max-md:hidden`,e(`Boards`)),y=t(`div`,`px-4 pb-5 pt-1`);m.append(t(`div`,`min-h-0 flex-1 overflow-y-auto`,v,y));async function b(e,t){let r=await s(`/api/boards/${e.slug}`,{public:t},`PATCH`);if(r.ok)return e.token=(await r.json().catch(()=>({}))).token??``,w();_=await n(r,`Could not change ${e.slug}`),await T()}async function x(e){_=await c(`/api/boards/${e}`,`DELETE`,`Could not delete ${e}`)??``,await T()}function S(e){let n=t(`div`,`group flex flex-col gap-1 py-2.5`),i=document.createElement(`a`);i.className=`min-w-0 flex-1 truncate text-[13px] font-medium text-neutral-800 hover:text-indigo-700 max-md:basis-full`,i.href=p(e),i.target=`_blank`,i.rel=`noreferrer`,i.textContent=e.title;let a=t(`div`,`flex flex-wrap items-center gap-x-2 gap-y-1`,i),s=r(``,``,e.public,t=>{e.public=t,b(e,t)});s.className=`flex flex-none cursor-pointer items-center gap-1.5 text-[11.5px] text-neutral-500`,s.append(t(`span`,``,`Public`));let c=d(`Delete`,`flex-none cursor-pointer text-[11.5px] text-neutral-400 opacity-0 transition-opacity hover:text-red-600 group-hover:opacity-100 pointer-coarse:opacity-100`);c.title=`Renames the folder on disk; nothing is erased`,c.onclick=()=>void x(e.slug);let l=f(`flex-none cursor-pointer text-[11.5px] text-neutral-400 opacity-0 transition-opacity hover:text-indigo-600 group-hover:opacity-100 pointer-coarse:opacity-100`,()=>`${location.origin}${p(e)}`);l.title=`Copy the board's link`,a.append(t(`div`,`ml-auto flex flex-none items-center gap-2`,s,l,c)),n.append(a);let u=t(`div`,`flex flex-wrap items-center gap-x-3 gap-y-1 text-[11.5px] text-neutral-500`);u.append(t(`span`,`min-w-0 break-all font-mono text-neutral-400`,e.slug)),e.description&&u.append(t(`span`,`min-w-0 truncate`,e.description)),u.append(t(`span`,`flex-none text-neutral-400`,o(Date.parse(e.updatedAt))));for(let t of e.sessions){let e=d(t.slice(0,8),`flex-none cursor-pointer font-mono text-indigo-600 hover:underline`);e.title=`Open session ${t}`,e.onclick=()=>h(t),u.append(e)}if(e.public&&e.token){let n=t(`span`,`min-w-0 break-all font-mono text-neutral-400`,p(e));n.title=`Public URL — anyone holding this link can read the board, no password`,u.append(n)}return n.append(u),n}let C=[{title:`Public`,hint:`Readable at /p/<slug>-<token>/ by anyone holding the link.`,wanted:!0},{title:`Private`,hint:`Reachable from the Console only.`,wanted:!1}];function w(){let e=t(`div`,`mx-auto flex w-full max-w-3xl flex-col gap-4`);g.length===0&&e.append(a(`No boards yet. Ask an agent to build one — it writes a folder under ~/.pier/boards.`));for(let{title:n,hint:r,wanted:i}of C){let a=g.filter(e=>e.public===i);a.length!==0&&e.append(u(n,r,t(`div`,`flex flex-col divide-y divide-neutral-200/70`,...a.map(S))))}y.replaceChildren(..._?[t(`p`,`mb-3 text-[13px] text-red-600`,_)]:[],e)}async function T(){let e=await l(`/api/boards`,`Could not load boards`);if(!e.ok)return _=e.error,w();g=e.value,w(),_=``}let E=i(m,()=>void T());return window.addEventListener(`focus`,()=>{E.visible&&T()}),E}export{m as createBoardsView};
|
|
Binary file
|
|
Binary file
|
|
@@ -1,4 +0,0 @@
|
|
|
1
|
-
import{$ as e,B as t,H as n,J as r,L as i,R as a,U as o,a as s,at as c,c as l,d as u,et as d,f,i as p,it as m,o as h,p as g}from"./index-DbFu15NN.js";var ee=[[`path`,{d:`M12 5v14`}],[`path`,{d:`m19 12-7 7-7-7`}]],te=[[`path`,{d:`M12 22v-6`}],[`path`,{d:`M12 8V2`}],[`path`,{d:`M4 12H2`}],[`path`,{d:`M10 12H8`}],[`path`,{d:`M16 12h-2`}],[`path`,{d:`M22 12h-2`}],[`path`,{d:`m15 19-3-3-3 3`}],[`path`,{d:`m15 5-3 3-3-3`}]],ne=[[`path`,{d:`M10 20a1 1 0 0 0 .553.895l2 1A1 1 0 0 0 14 21v-7a2 2 0 0 1 .517-1.341L21.74 4.67A1 1 0 0 0 21 3H3a1 1 0 0 0-.742 1.67l7.225 7.989A2 2 0 0 1 10 14z`}]],re=[[`path`,{d:`M15 6a9 9 0 0 0-9 9V3`}],[`circle`,{cx:`18`,cy:`6`,r:`3`}],[`circle`,{cx:`6`,cy:`18`,r:`3`}]],_=[[`path`,{d:`M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8`}],[`path`,{d:`M3 3v5h5`}]],v=e=>{let t=new Date(e),n=e=>String(e).padStart(2,`0`);return`${t.getFullYear()}-${n(t.getMonth()+1)}-${n(t.getDate())} ${n(t.getHours())}:${n(t.getMinutes())}:${n(t.getSeconds())}`},ie=e=>[e.subject,e.body,[[e.author,e.email?`<${e.email}>`:``].filter(Boolean).join(` `),e.at?v(e.at):``].filter(Boolean).join(` · `)].filter(Boolean).join(`
|
|
2
|
-
|
|
3
|
-
`),y=null;function b(){y&&(y.remove(),y=null,document.removeEventListener(`pointerdown`,b,!0),window.removeEventListener(`scroll`,b,!0))}function x(e,t){e.onmouseenter=()=>{b();let r=t();if(!r)return;y=n(`div`,`pointer-events-none fixed z-50 max-w-96 whitespace-pre-wrap rounded-lg bg-neutral-800/95 px-3 py-2 font-sans text-[11.5px] leading-snug text-neutral-100 shadow-lg`),y.textContent=r,document.body.append(y),document.addEventListener(`pointerdown`,b,!0),window.addEventListener(`scroll`,b,!0);let i=e.getBoundingClientRect();y.style.left=`${Math.max(8,Math.min(i.left,window.innerWidth-y.offsetWidth-8))}px`,y.style.top=i.bottom+6+y.offsetHeight>window.innerHeight?`${i.top-y.offsetHeight-6}px`:`${i.bottom+6}px`},e.onmouseleave=b}function S(t,r,i){let a=n(`button`,`flex w-full cursor-pointer items-center gap-1.5 px-2 py-1 text-left hover:bg-neutral-100`,e(m,`h-3 w-3 text-indigo-600 ${t.value===r?``:`invisible`}`),n(`span`,`inline-flex flex-none items-center gap-1.5 font-mono text-[11.5px] text-neutral-800`,t.label??t.value));return t.subject&&a.append(n(`span`,`min-w-0 truncate text-[11px] text-neutral-400`,t.subject)),t.author&&a.append(n(`span`,`ml-auto flex-none pl-1.5 text-[10.5px] text-neutral-400`,`${t.author} · ${t.at?o(t.at):``}`)),x(a,()=>ie(t)),a.onclick=()=>i(t.value),a}function ae(t,r,i,a,o){let{base:c,head:l}=i,u=()=>l&&c===`${l}~1`?l:``,d=u()?`commit`:`compare`,f=r.commits[0],p=e=>[...e?[{value:``,label:`Working tree`,subject:`the files on disk, uncommitted changes included`}]:[],{...f,value:`HEAD`},...r.refs.map(e=>({value:e.name,subject:e.subject})),...r.commits.map(e=>({...e,value:e.hash}))],m=n(`div`,`flex w-[38rem] max-w-full flex-col`);function g(){let t=(e,t)=>{let r=n(`button`,`cursor-pointer rounded-full px-2.5 py-0.5 text-[11.5px] ${d===t?`bg-indigo-50 font-medium text-indigo-700`:`text-neutral-500 hover:bg-neutral-100`}`,e);return r.onclick=()=>{d=t,g()},r},i=(e,t,r,i)=>n(`div`,`flex min-w-0 flex-1 flex-col`,n(`div`,`flex-none px-2 pb-0.5 text-[10.5px] font-semibold uppercase tracking-wide text-neutral-400`,e),n(`div`,`min-h-0 flex-1 overflow-y-auto`,...t.map(e=>S(e,r,i)))),f=d===`compare`?n(`div`,`flex h-72 min-h-0 divide-x divide-neutral-200 pb-1`,i(`base`,p(!1),c,e=>{c=e,g(),a(c,l)}),i(`to`,p(!0),l,e=>{l=e,g(),a(c,l)})):n(`div`,`flex h-72 min-h-0 flex-col overflow-y-auto pb-1`,...r.commits.map(e=>S({value:e.hash,...e},u(),e=>{c=`${e}~1`,l=e,s(),a(c,l)}))),h=n(`div`,`flex-none border-t border-neutral-200`,S({value:o,label:`${o} ↔ Working tree`,subject:`back to the default: the main line's tip vs the files on disk`},c===o&&l===``?o:``,()=>{c=o,l=``,g(),a(c,l)}));h.querySelector(`button > span`).prepend(e(_)),m.replaceChildren(n(`div`,`flex flex-none items-center gap-1 border-b border-neutral-200 px-2 py-1.5`,t(`Compare two points`,`compare`),t(`One commit`,`commit`)),f,...d===`compare`?[h]:[])}g(),h(t,m)}var oe=/\.(png|jpe?g|gif|webp|avif|bmp|svg)$/i,se={A:`text-emerald-600`,M:`text-amber-600`,D:`text-red-600`},ce=30,C=`pier.filesPrefs`,w=50,T=()=>{try{let e=JSON.parse(localStorage.getItem(C)??`{}`);return e&&typeof e==`object`?e:{}}catch{return{}}},E=e=>{let t=e?T()[e]:void 0;return typeof t?.cwd==`string`&&typeof t.base==`string`&&typeof t.head==`string`?{cwd:t.cwd,base:t.base,head:t.head}:null},le=(e,t)=>{let n=T();delete n[e],n[e]=t;let r=Object.keys(n);for(let e of r.slice(0,Math.max(0,r.length-w)))delete n[e];try{localStorage.setItem(C,JSON.stringify(n))}catch(e){console.warn(`Files: could not remember this folder`,e)}},D=(e,t=`indigo`)=>{let r=n(`button`,`min-w-0 cursor-pointer truncate rounded-md px-2 py-0.5 text-left font-mono text-[11.5px] ${t===`indigo`?`bg-indigo-50 font-medium text-indigo-700 hover:bg-indigo-100`:`bg-neutral-100 text-neutral-600 hover:bg-neutral-200`}`,e);return r.type=`button`,r};function O(o,s,m,h){let _=``,v=``,y={branch:null,refs:[],commits:[],worktrees:[]},b=`HEAD`,S=``,C=new Map,w=!0,T=!1,O=[],k=new Set,A=null,j=null,ue=n(`header`,`pagehead`),M=n(`div`,`flex-none border-b border-neutral-200`),N=n(`div`,`min-h-0 flex-1 overflow-y-auto py-1`),de=n(`aside`,`flex w-64 flex-none flex-col border-r border-neutral-200 text-[12.5px] max-md:h-2/5 max-md:w-full max-md:border-b max-md:border-r-0`,M,N),P=n(`div`,`h-full min-w-0 overflow-auto`),F=n(`div`,`pointer-events-none absolute inset-y-0 right-0 z-20 hidden w-2`),fe=n(`section`,`relative min-h-0 min-w-0 flex-1`,P,F);o.append(ue,n(`div`,`flex min-h-0 flex-1 max-md:flex-col`,de,fe));let I=(e,t)=>`/api/explorer/${e}?${new URLSearchParams({root:_,...t})}`,L=(e,t)=>`/api/fs/${e}?${new URLSearchParams({root:_,...t})}`,R=(e,t=`text-neutral-400`)=>n(`p`,`px-4 py-3 text-[12.5px] ${t}`,e),z=e=>{let t=0;for(let n of C.keys())n.startsWith(`${e}/`)&&t++;return t};function B(e,t){let n=new Set(t.map(e=>e.name)),r=e?`${e}/`:``;for(let[e,i]of C){if(i.status!==`D`||!e.startsWith(r))continue;let a=e.slice(r.length).split(`/`)[0];n.has(a)||(n.add(a),t.push({name:a,dir:e.slice(r.length).includes(`/`)}))}let i=t;return w&&y.branch&&(i=t.filter(e=>e.dir?z(`${r}${e.name}`)>0:C.has(`${r}${e.name}`))),i.sort((e,t)=>Number(t.dir)-Number(e.dir)||e.name.localeCompare(t.name)),i.map(e=>e.dir?pe(`${r}${e.name}`,e.name):me(`${r}${e.name}`,e.name))}async function V(e,t){try{let{entries:n}=await r(L(`ls`,{path:t}),`could not list this folder`);e.replaceChildren(...B(t,n)),e.childElementCount||e.append(R(w?`No changes.`:`Empty.`))}catch(n){let r=B(t,[]);r.length?e.replaceChildren(...r):e.replaceChildren(R(String(n),`text-red-600`))}}function pe(e,r){let i=z(e),a=[n(`span`,`truncate`,r)];i&&a.push(n(`span`,`ml-auto flex-none rounded bg-amber-100/80 px-1 font-mono text-[10px] text-amber-700`,String(i)));let{el:o,summary:s}=t(``,a);s.className+=` px-2 py-0.5 hover:bg-neutral-100`;let c=n(`div`,`pl-3`);o.append(c);let l=!1,u=()=>{l||(l=!0,c.append(R(`…`)),V(c,e))};return o.ontoggle=()=>{k[o.open?`add`:`delete`](e),o.open&&u()},(w&&y.branch&&!T||k.has(e))&&(o.open=!0,u()),o}function H(e){j?.classList.remove(`bg-indigo-50`,`text-indigo-700`),j=e,e.classList.add(`bg-indigo-50`,`text-indigo-700`)}function me(e,t){let r=C.get(e)?.status,i=n(`button`,`flex w-full cursor-pointer items-center gap-1.5 px-2 py-0.5 pl-6 text-left hover:bg-neutral-100`,n(`span`,`truncate`,t));return r&&i.append(n(`span`,`ml-auto flex-none font-mono text-[10.5px] font-semibold ${se[r]??`text-neutral-500`}`,r)),i.title=e,e===A&&H(i),i.onclick=()=>{H(i),A=e,_e(e)},i}let U=()=>void V(N,``),W=(e,t)=>[...e?[n(`span`,`flex-none font-mono text-[10.5px] font-semibold text-emerald-600`,`+${e}`)]:[],...t?[n(`span`,`flex-none font-mono text-[10.5px] font-semibold text-red-600`,`−${t}`)]:[]];function G(e,t,r){let a=C.get(e),o=n(`div`,`sticky top-0 z-10 flex items-center gap-2 border-b border-neutral-200 bg-white px-4 py-1.5 font-mono text-[11.5px] text-neutral-500`,n(`span`,`truncate`,e));a&&(o.append(n(`span`,`flex-none font-semibold ${se[a.status]??``}`,a.status)),o.append(...W(a.add,a.del)));let s=n(`div`,`ml-auto flex flex-none items-center gap-2`);if(r&&s.append(r),t){let t=document.createElement(`a`);t.className=`flex-none text-neutral-400 hover:text-indigo-700 hover:underline`,t.href=L(`file`,{path:e}),t.download=i(e),t.textContent=`Download`,s.append(t)}return o.append(s),o}function he(e){let t=0;for(;t<e.length;){if(e[t].tone!==`del`){t++;continue}let n=t;for(;t<e.length&&e[t].tone===`del`;)t++;let r=t;for(;t<e.length&&e[t].tone===`add`;)t++;let i=Math.min(r-n,t-r);for(let t=0;t<i;t++){let i=e[n+t],a=e[r+t],o=Math.min(i.text.length,a.text.length),s=0;for(;s<o&&i.text[s]===a.text[s];)s++;let c=0;for(;c<o-s&&i.text[i.text.length-1-c]===a.text[a.text.length-1-c];)c++;s+c!==0&&(i.mark=[s,i.text.length-c],a.mark=[s,a.text.length-c])}}}let K=we();function q(){F.classList.add(`hidden`),F.replaceChildren(),K.set([],[])}let ge=0,J=e=>e===ge;function _e(e){let t=++ge;return C.has(e)?Te(e,t):ve(e,t)}async function ve(e,t){if(!J(t))return;q();let r=L(`file`,{path:e}),i=n(`div`,`min-w-0`);if(P.classList.remove(`flex`,`flex-col`),P.replaceChildren(G(e,!0),i),oe.test(e)){let e=n(`img`,`max-w-full p-4`);e.src=r,e.onerror=()=>i.replaceChildren(R(`Could not load image.`,`text-red-600`)),i.append(e);return}if(e.toLowerCase().endsWith(`.pdf`)){let t=document.createElement(`iframe`);t.className=`h-full w-full`,t.src=r,P.replaceChildren(G(e,!0),t),P.classList.add(`flex`,`flex-col`);return}i.append(R(`…`));let a=await fetch(r);if(!J(t))return;if(!a.ok){let{error:e}=await a.json().catch(()=>({}));i.replaceChildren(R(e??`HTTP ${a.status}`,`text-red-600`));return}if(!(a.headers.get(`content-type`)??``).startsWith(`text/plain`)){i.replaceChildren(R(`Binary file — use Download.`));return}let o=f(await a.text());if(!J(t))return;let s=await g(e);J(t)&&i.replaceChildren(u(o,s))}function ye(e){let t=[],n=0,r=0,i=!1,a=e.split(`
|
|
4
|
-
`);a.at(-1)===``&&a.pop();for(let e of a){let a=/^@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@/.exec(e);if(a){i&&t.push({nums:[``,``],text:`⋯`,tone:``}),[n,r]=[Number(a[1]),Number(a[2])],i=!0;continue}!i||e.startsWith(`\\`)||(e.startsWith(`+`)?t.push({nums:[``,r++],text:e.slice(1),tone:`add`}):e.startsWith(`-`)?t.push({nums:[n++,``],text:e.slice(1),tone:`del`}):t.push({nums:[n++,r++],text:e.slice(1),tone:``}))}return t}function be(e){let t=[];for(let n=0;n<e.length;n++){let r=e[n].tone;if(!r)continue;let i=t.at(-1);i&&i.end===n-1?(i.end=n,(r===`add`&&i.tone===`del`||r===`del`&&i.tone===`add`)&&(i.tone=`mixed`)):t.push({start:n,end:n,tone:r})}return t}let xe={add:`bg-emerald-400`,del:`bg-red-400`,mixed:`bg-amber-400`};function Se(e,t,r){F.replaceChildren(...e.map(e=>{let i=n(`button`,`pointer-events-auto absolute w-full cursor-pointer ${xe[e.tone]}`);return i.style.top=`${e.start/t*100}%`,i.style.height=`${Math.max(.4,(e.end-e.start+1)/t*100)}%`,i.title=`line ${Ce(r,e.start)}`,i.onclick=()=>r[e.start].scrollIntoView({block:`center`}),i})),F.classList.remove(`hidden`)}let Ce=(e,t)=>e[t]?.querySelector(`span`)?.textContent?.trim()||String(t+1);function we(){let t=[],r=[],i=-1,a=n(`span`,`flex-none text-[10.5px] text-neutral-400`),s=e=>{t.length&&(i=(i+e+t.length)%t.length,r[t[i].start].scrollIntoView({block:`center`}),a.textContent=`${i+1}/${t.length}`)},u=()=>t.length>0&&!o.classList.contains(`hidden`),d=(t,r,i,a)=>{let o=n(`button`,`icon-btn flex-none`,e(t));return o.onclick=()=>s(r),l(o,a,i,()=>s(r),u),o};return{el:n(`div`,`flex flex-none items-center gap-1`,a,d(c,-1,`Previous change`,[`p`,`k`]),d(ee,1,`Next change`,[`n`,`j`])),set(e,n){t=e,r=n,i=-1,a.textContent=`${e.length} change${e.length===1?``:`s`}`}}}async function Te(e,t){q(),P.classList.remove(`flex`,`flex-col`);let n=C.get(e)?.status!==`D`;P.replaceChildren(G(e,n),R(`…`));try{let{diff:i}=await r(I(`diff`,{base:b,head:S,file:e,context:`99999`}),`could not read this diff`);if(!J(t))return;if(!i||/^Binary files /m.test(i))return ve(e,t);let a=ye(i);he(a);let o=await g(e);if(!J(t))return;let s=u(a,o),c=[...s.children],l=be(a);K.set(l,c),P.replaceChildren(G(e,n,K.el),s),Se(l,a.length,c)}catch(n){J(t)&&P.replaceChildren(G(e,!1),R(String(n),`text-red-600`))}}let Y=()=>S&&b===`${S}~1`?S:``,Ee=()=>y.commits.find(e=>e.hash===Y())?.subject??``,De=()=>Y()?`${Y()} — ${Ee()||`commit`}`:`${b} ↔ ${S||`working tree`}`,Oe=(t,r,i,a)=>{let o=n(`button`,`flex h-6 w-6 flex-none cursor-pointer items-center justify-center rounded-md ${i?`bg-indigo-50 text-indigo-700`:`text-neutral-400 hover:bg-neutral-100 hover:text-neutral-700`}`);return o.append(e(t)),o.title=r,o.setAttribute(`aria-label`,r),o.setAttribute(`aria-pressed`,String(i)),o.onclick=a,o};function X(){let e=Oe(ne,`Show only files that differ`,w,()=>{w=!w,X(),U()}),t=Oe(te,T?`Restore folders`:`Collapse all folders`,T,()=>{if(T=!T,T)O=[...k],k.clear();else for(let e of O)k.add(e);X(),U()});if(!y.branch){M.replaceChildren(n(`div`,`flex items-center justify-end gap-1 p-1.5`,t));return}let r=D(De());if(Y()){let e=y.commits.find(e=>e.hash===Y());x(r,()=>e?ie(e):``)}else r.title=`Choose what to diff`;r.onclick=async()=>{await Me(),ae(r,y,{base:b,head:S},(e,t)=>{b=e,S=t,Z()},$())};let[i,a]=[0,0];for(let e of C.values())i+=e.add,a+=e.del;M.replaceChildren(n(`div`,`flex flex-col gap-1 p-2`,r,n(`div`,`flex items-center gap-1.5 pl-1`,n(`span`,`text-[11px] text-neutral-400`,`${C.size} changed file${C.size===1?``:`s`}`),...W(i,a),n(`span`,`ml-auto`),e,t)))}let ke=()=>{v&&_&&le(v,{cwd:_,base:b,head:S})},Ae=e=>{let t=e.replace(/~\d+$/,``);return!t||t===`HEAD`||y.refs.some(e=>e.name===t)||y.commits.some(e=>e.hash===t)};async function je(){if(C=new Map,!y.branch)return;let{files:e}=await r(I(`diff`,{base:b,head:S}),`could not read what changed`);for(let t of e)C.set(t.path,{status:t.status,add:t.add,del:t.del});if(C.size<=ce)for(let e of C.keys()){let t=e.split(`/`);for(let e=1;e<t.length;e++)k.add(t.slice(0,e).join(`/`))}}async function Z(){ke();try{await je()}catch(e){X(),N.replaceChildren(R(String(e),`text-red-600`));return}X(),U(),A&&_e(A)}function Q(){let t=D(_||`Choose a folder…`,`neutral`);t.className+=` max-w-72`,t.title=`Switch folder`,t.onclick=()=>p(t,y.worktrees.map(e=>({path:e.path,...e.branch?{hint:e.branch}:{}})),_||void 0,m);let r=n(`button`,`icon-btn`,e(d));r.type=`button`,r.title=`Close Files`,r.setAttribute(`aria-label`,`Close Files`),r.onclick=h,r.classList.add(`ml-auto`),ue.replaceChildren(t,n(`span`,`flex min-w-0 items-center gap-1 truncate font-mono text-[11.5px] text-neutral-400`,...y.branch?[e(re),n(`span`,`truncate`,y.branch)]:[`no git`]),r)}async function Me(){if(_)try{y=await r(I(`git`,{}),`could not read this repository`),Q()}catch(e){P.replaceChildren(R(String(e),`text-red-600`))}}let $=()=>y.refs.some(e=>e.name===`main`)?`main`:y.refs.some(e=>e.name===`master`)?`master`:`HEAD`;async function Ne(){if(Q(),A=null,j=null,k.clear(),q(),P.classList.remove(`flex`,`flex-col`),P.replaceChildren(R(`Select a file.`)),!_){M.replaceChildren(),N.replaceChildren(),P.replaceChildren(R(`No folder yet — pick one from the chip above.`));return}try{y=await r(I(`git`,{}),`could not read this repository`)}catch(e){y={branch:null,refs:[],commits:[],worktrees:[]},P.replaceChildren(R(String(e),`text-red-600`))}let e=E(v),t=e&&e.cwd===_&&Ae(e.base)&&Ae(e.head)?e:null;b=t?.base??$(),S=t?.head??``,Q(),await Z()}return a(o,e=>{let t=s(),n=t?.id??``,r=e?.startsWith(`/`)?e:(n===v?_:``)||E(n)?.cwd||t?.cwd||``;if(r===_&&n===v&&N.childElementCount){Q(),Me().then(Z);return}v=n,_=r,Ne()},()=>{q(),P.classList.remove(`flex`,`flex-col`),P.replaceChildren(R(`Select a file.`))})}export{O as createExplorerView};
|
|
Binary file
|
|
Binary file
|