@timqi/pier 0.1.4 → 0.1.5
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 +4 -4
- package/dist/agent/events.js +12 -3
- package/dist/agent/listing.js +10 -2
- package/dist/agent/packages.js +3 -13
- package/dist/agent/pi.js +13 -40
- package/dist/channels/attach.js +2 -2
- package/dist/channels/commands.js +10 -8
- package/dist/channels/config.js +4 -6
- package/dist/channels/control.js +60 -14
- package/dist/channels/conversations.js +60 -13
- package/dist/channels/handoff.js +90 -0
- package/dist/channels/lark-api.js +7 -0
- package/dist/channels/lark-outbound.js +19 -0
- package/dist/channels/lark-panel.js +41 -26
- package/dist/channels/lark-render.js +2 -1
- package/dist/channels/lark.js +35 -26
- package/dist/channels/lines.js +1 -1
- package/dist/channels/panel.js +324 -115
- package/dist/channels/receipts.js +25 -12
- package/dist/channels/routes.js +21 -4
- package/dist/channels/runtime.js +21 -7
- package/dist/channels/slack-outbound.js +10 -0
- package/dist/channels/slack-panel.js +81 -40
- package/dist/channels/slack-render.js +3 -1
- package/dist/channels/slack.js +29 -14
- package/dist/channels/types.js +1 -2
- package/dist/cli.js +13 -5
- package/dist/core/identity.js +47 -12
- package/dist/core/reply.js +4 -1
- package/dist/core/router.js +20 -3
- package/dist/db.js +6 -1
- package/dist/main.js +42 -9
- package/dist/settings.js +3 -12
- package/dist/socket.js +10 -6
- package/dist/tasks/cli.js +17 -7
- package/dist/tasks/operations.js +24 -12
- package/dist/tools.js +1 -3
- package/dist/web/public/assets/{activity-NLS8W9yl.js → activity-CrybM-E8.js} +1 -1
- package/dist/web/public/assets/activity-CrybM-E8.js.br +0 -0
- package/dist/web/public/assets/activity-CrybM-E8.js.gz +0 -0
- package/dist/web/public/assets/{boards-DleegoiC.js → boards-Cw7_6J6L.js} +1 -1
- package/dist/web/public/assets/boards-Cw7_6J6L.js.br +0 -0
- package/dist/web/public/assets/boards-Cw7_6J6L.js.gz +0 -0
- package/dist/web/public/assets/{explorer-vJvV1Sx0.js → explorer-DV066dUD.js} +1 -1
- package/dist/web/public/assets/explorer-DV066dUD.js.br +0 -0
- package/dist/web/public/assets/explorer-DV066dUD.js.gz +0 -0
- package/dist/web/public/assets/index-BrNHu2qj.js +85 -0
- package/dist/web/public/assets/index-BrNHu2qj.js.br +0 -0
- package/dist/web/public/assets/index-BrNHu2qj.js.gz +0 -0
- package/dist/web/public/assets/index-DLszkDUV.css +2 -0
- package/dist/web/public/assets/index-DLszkDUV.css.br +0 -0
- package/dist/web/public/assets/index-DLszkDUV.css.gz +0 -0
- package/dist/web/public/assets/{runs-C5FveZ75.js → runs-DDagTNaM.js} +1 -1
- package/dist/web/public/assets/runs-DDagTNaM.js.br +0 -0
- package/dist/web/public/assets/runs-DDagTNaM.js.gz +0 -0
- package/dist/web/public/assets/settings-BEdSeXpm.js +5 -0
- package/dist/web/public/assets/settings-BEdSeXpm.js.br +0 -0
- package/dist/web/public/assets/settings-BEdSeXpm.js.gz +0 -0
- package/dist/web/public/assets/{task-runs-CJJ2j5Ks.js → task-runs-BnMack9t.js} +1 -1
- package/dist/web/public/assets/task-runs-BnMack9t.js.br +0 -0
- package/dist/web/public/assets/task-runs-BnMack9t.js.gz +0 -0
- package/dist/web/public/assets/{tasks-R2Pv0Abg.js → tasks-BkW7YShZ.js} +1 -1
- package/dist/web/public/assets/tasks-BkW7YShZ.js.br +0 -0
- package/dist/web/public/assets/tasks-BkW7YShZ.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/push.js +4 -10
- package/dist/{extensions/web → websearch}/artifacts.js +2 -2
- package/dist/websearch/cli.js +73 -0
- package/dist/websearch/run.js +273 -0
- package/docs/deploy.md +3 -3
- package/package.json +2 -3
- package/skills/pier-help/SKILL.md +22 -10
- package/skills/pier-tasks/SKILL.md +17 -8
- package/skills/pier-web/SKILL.md +50 -0
- package/dist/channels/telegram-api.js +0 -86
- package/dist/channels/telegram-panel.js +0 -97
- package/dist/channels/telegram-render.js +0 -69
- package/dist/channels/telegram.js +0 -421
- package/dist/extensions/index.js +0 -10
- package/dist/extensions/web/index.js +0 -9
- package/dist/extensions/web/tools.js +0 -265
- 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.br +0 -0
- package/dist/web/public/assets/boards-DleegoiC.js.gz +0 -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 +0 -2
- 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 +0 -85
- 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.br +0 -0
- package/dist/web/public/assets/runs-C5FveZ75.js.gz +0 -0
- package/dist/web/public/assets/settings-nJHIBbAz.js +0 -5
- 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.br +0 -0
- package/dist/web/public/assets/task-runs-CJJ2j5Ks.js.gz +0 -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/{extensions/web → websearch}/anthropic.js +0 -0
- /package/dist/{extensions/web → websearch}/content.js +0 -0
- /package/dist/{extensions/web → websearch}/http.js +0 -0
- /package/dist/{extensions/web → websearch}/json.js +0 -0
- /package/dist/{extensions/web → websearch}/language.js +0 -0
- /package/dist/{extensions/web → websearch}/openai.js +0 -0
- /package/dist/{extensions/web → websearch}/provider.js +0 -0
package/dist/core/reply.js
CHANGED
|
@@ -30,7 +30,8 @@ added by Pier, not typed by them. It appears only on a change — new speaker, a
|
|
|
30
30
|
time only, like \`[14:23]\`. Use that \`id\` to mention someone; never ask for
|
|
31
31
|
their own. \`place\` is \`<platform>:<conversation>\` (Slack:
|
|
32
32
|
\`slack:<channel>/<thread_ts>\`), said once per session: the channel and thread a
|
|
33
|
-
script takes.
|
|
33
|
+
script takes. Where no tool of yours takes that platform's ids, the header
|
|
34
|
+
carries neither and reads \`[name time platform]\`.
|
|
34
35
|
`;
|
|
35
36
|
/** Deployment facts an agent cannot discover: a guessed path is wrong wherever
|
|
36
37
|
* `PIER_HOME` moved and fails as "nothing is configured"; GPT models carry
|
|
@@ -102,6 +103,8 @@ export const quietLabel = (silence) => silence ? `stayed silent — ${silence}`
|
|
|
102
103
|
export const isSilentReply = (reply) => !reply.text.trim() && reply.suggestions.length === 0;
|
|
103
104
|
/** How a reasoning level is spelled wherever a human reads it. */
|
|
104
105
|
export const thinkingLabel = (level) => level === "xhigh" ? "Extra high" : level[0].toUpperCase() + level.slice(1);
|
|
106
|
+
/** `max` characters, the last one an ellipsis when something was cut. */
|
|
107
|
+
export const cut = (text, max) => (text.length > max ? `${text.slice(0, max - 1)}…` : text);
|
|
105
108
|
/** 1200 → "1.2K", 12_000 → "12K" — absolute token counts read badly inline. */
|
|
106
109
|
export const compact = (n) => {
|
|
107
110
|
if (n < 1000)
|
package/dist/core/router.js
CHANGED
|
@@ -52,6 +52,7 @@ export class Router {
|
|
|
52
52
|
hub;
|
|
53
53
|
resolve;
|
|
54
54
|
sessionIdOf;
|
|
55
|
+
chatKeyOf;
|
|
55
56
|
byKey = new Map();
|
|
56
57
|
bySession = new Map();
|
|
57
58
|
/** Resolves in flight: two surfaces asking at once must share one session
|
|
@@ -70,10 +71,14 @@ export class Router {
|
|
|
70
71
|
/** The durable chat → session mapping, read before opening: a chat and the
|
|
71
72
|
* workbench asking for one transcript must share one lock and one object.
|
|
72
73
|
* Undefined for a chat that has none yet. */
|
|
73
|
-
sessionIdOf = () => undefined
|
|
74
|
+
sessionIdOf = () => undefined,
|
|
75
|
+
/** The inverse: the durable chat of a session an alias is opening, so the
|
|
76
|
+
* chat is the delivery key from the first turn (wired in main.ts). */
|
|
77
|
+
chatKeyOf = () => undefined) {
|
|
74
78
|
this.hub = hub;
|
|
75
79
|
this.resolve = resolve;
|
|
76
80
|
this.sessionIdOf = sessionIdOf;
|
|
81
|
+
this.chatKeyOf = chatKeyOf;
|
|
77
82
|
}
|
|
78
83
|
registerChannel(channel) {
|
|
79
84
|
this.channels.set(channel.id, channel);
|
|
@@ -233,7 +238,11 @@ export class Router {
|
|
|
233
238
|
// triggers, so the answer has a visible cause. The hub carries it whole.
|
|
234
239
|
if (payload.type === "system-input") {
|
|
235
240
|
const channel = this.channels.get(key.channelId);
|
|
236
|
-
channel?.notify(key.conversationId, {
|
|
241
|
+
channel?.notify(key.conversationId, {
|
|
242
|
+
text: digest(payload.text),
|
|
243
|
+
origin: payload.origin,
|
|
244
|
+
at: payload.at,
|
|
245
|
+
})
|
|
237
246
|
.catch((err) => {
|
|
238
247
|
log.error(`notify ${key.channelId} failed`, err);
|
|
239
248
|
this.hub.emit(session.id, {
|
|
@@ -263,6 +272,13 @@ export class Router {
|
|
|
263
272
|
}),
|
|
264
273
|
};
|
|
265
274
|
this.bySession.set(session.id, attached);
|
|
275
|
+
// The durable chat outranks the alias that happened to open the session
|
|
276
|
+
// first (a restart, the web speaking first), same rule as `reached`.
|
|
277
|
+
const chat = isAlias(key) ? this.chatKeyOf(session.id) : undefined;
|
|
278
|
+
if (chat) {
|
|
279
|
+
this.byKey.set(keyOf(chat), session);
|
|
280
|
+
attached.key = chat;
|
|
281
|
+
}
|
|
266
282
|
}
|
|
267
283
|
/** An adapter's send is several platform calls (chunks, then attachments),
|
|
268
284
|
* so two answers left to overlap interleave in the chat. Per conversation:
|
|
@@ -570,7 +586,8 @@ export class Router {
|
|
|
570
586
|
// A chat is named so the agent can hand it to a script (skills/pier-slack);
|
|
571
587
|
// an alias names nothing a shell could reach.
|
|
572
588
|
const where = isAlias(msg.key) ? undefined : keyOf(msg.key);
|
|
573
|
-
const
|
|
589
|
+
const opaque = this.channels.get(msg.key.channelId)?.opaqueIds;
|
|
590
|
+
const prompt = withPrefix(this.senders.next(session.id, msg.sender, Date.now(), where, opaque), text);
|
|
574
591
|
log.debug(`${action} ${keyOf(msg.key)} → session ${session.id} (${String(prompt.length)} chars)`);
|
|
575
592
|
// A rejected call surfaces on the event stream, never as a throw across the seam.
|
|
576
593
|
session[action](prompt).catch((err) => {
|
package/dist/db.js
CHANGED
|
@@ -288,7 +288,6 @@ const MIGRATIONS = [
|
|
|
288
288
|
-- every migration. A name filed in the vault first wins (DO NOTHING); the
|
|
289
289
|
-- channel copy is dropped either way.
|
|
290
290
|
WITH moved(platform, path, name) AS (VALUES
|
|
291
|
-
('telegram', '$.token', 'TELEGRAM_TOKEN'),
|
|
292
291
|
('slack', '$.token', 'SLACK_TOKEN'), ('slack', '$.appToken', 'SLACK_APP_TOKEN'),
|
|
293
292
|
('lark', '$.token', 'LARK_APP_ID'), ('lark', '$.appToken', 'LARK_APP_SECRET'))
|
|
294
293
|
INSERT INTO vault(name, value, updated_at)
|
|
@@ -300,6 +299,12 @@ const MIGRATIONS = [
|
|
|
300
299
|
`,
|
|
301
300
|
// 25 — the mid-run decision channel is gone; a control message is steer or follow_up.
|
|
302
301
|
`DELETE FROM task_messages WHERE json_extract(json, '$.kind') IN ('progress', 'decision', 'reply');`,
|
|
302
|
+
// 26 — a session created from the panel is re-created from its own launch
|
|
303
|
+
// record (channels/conversations.ts); the index serves the session → thread lookups.
|
|
304
|
+
`
|
|
305
|
+
ALTER TABLE conversations ADD COLUMN launch TEXT;
|
|
306
|
+
CREATE INDEX conversations_session ON conversations(session_id);
|
|
307
|
+
`,
|
|
303
308
|
];
|
|
304
309
|
/** `BEGIN IMMEDIATE`: taking the write lock up front turns a race with another
|
|
305
310
|
* Pier process into a wait, where deferred would be SQLITE_BUSY halfway through. */
|
package/dist/main.js
CHANGED
|
@@ -15,16 +15,17 @@ import { defaultBoardsDir, registerBoardRoutes } from "./boards/boards.js";
|
|
|
15
15
|
import { ChannelStore } from "./channels/config.js";
|
|
16
16
|
import { createControl } from "./channels/control.js";
|
|
17
17
|
import { ConversationStore, resolveConversation } from "./channels/conversations.js";
|
|
18
|
+
import { createHandoff } from "./channels/handoff.js";
|
|
18
19
|
import { registerChannelRoutes } from "./channels/routes.js";
|
|
19
20
|
import { ChannelRuntime } from "./channels/runtime.js";
|
|
20
21
|
import { EventHub } from "./core/hub.js";
|
|
21
22
|
import { splitSpeaker } from "./core/identity.js";
|
|
22
23
|
import { pierDb } from "./db.js";
|
|
23
24
|
import { deliverLedger, drainForRestart, RestartLedger } from "./drain.js";
|
|
24
|
-
import { BUNDLED } from "./extensions/index.js";
|
|
25
25
|
import { surfacePrompt } from "./core/reply.js";
|
|
26
26
|
import { Router } from "./core/router.js";
|
|
27
27
|
import { acquireInstanceLock } from "./lock.js";
|
|
28
|
+
import { parseWebParams, runWeb } from "./websearch/run.js";
|
|
28
29
|
import { logger } from "./log.js";
|
|
29
30
|
import { registerTaskRoutes } from "./tasks/routes.js";
|
|
30
31
|
import { TaskService } from "./tasks/service.js";
|
|
@@ -106,7 +107,7 @@ const router = new Router(hub, (key) => {
|
|
|
106
107
|
return factory.resume(key.conversationId);
|
|
107
108
|
}
|
|
108
109
|
return resolveIm(key);
|
|
109
|
-
}, (key) => conversations.get(key));
|
|
110
|
+
}, (key) => conversations.get(key), (id) => conversations.keyOf(id));
|
|
110
111
|
const stopEviction = router.startIdleEviction();
|
|
111
112
|
tasks = new TaskService(new TaskStore(db), factory, router, hub, {
|
|
112
113
|
modelMenu: () => settings.get().modelMenu,
|
|
@@ -123,9 +124,34 @@ const packages = new PiPackageStore(piConfig, { version: currentVersion(), setti
|
|
|
123
124
|
// At boot, not lazily: the answer waits for the next Console open (update.ts).
|
|
124
125
|
packages.watchUpdates();
|
|
125
126
|
channelStore = new ChannelStore(db, vault);
|
|
126
|
-
const
|
|
127
|
-
const
|
|
128
|
-
|
|
127
|
+
const sessionState = new SessionStateStore(db);
|
|
128
|
+
const control = createControl({
|
|
129
|
+
router, factory, conversations, store: channelStore,
|
|
130
|
+
modelMenu: () => settings.get().modelMenu,
|
|
131
|
+
});
|
|
132
|
+
// The panel pulls through the handoff and the handoff posts through the
|
|
133
|
+
// runtime: the runtime's half is reached lazily so both can be built.
|
|
134
|
+
const handoff = createHandoff({
|
|
135
|
+
store: channelStore,
|
|
136
|
+
runtime: {
|
|
137
|
+
running: () => channels.running(),
|
|
138
|
+
openThread: (platform, chatId, note) => channels.openThread(platform, chatId, note),
|
|
139
|
+
},
|
|
140
|
+
conversations,
|
|
141
|
+
factory,
|
|
142
|
+
router,
|
|
143
|
+
hub,
|
|
144
|
+
publicUrl: () => settings.get().publicUrl,
|
|
145
|
+
taskSessions: () => tasks.taskSessions(),
|
|
146
|
+
workingSet: () => sessionState.flags(),
|
|
147
|
+
log: (m) => logger("channels").info(m),
|
|
148
|
+
});
|
|
149
|
+
const channels = new ChannelRuntime(channelStore, router, control, handoff);
|
|
150
|
+
resolveIm = resolveConversation(conversations, factory, control.launchFor, (key, message) => {
|
|
151
|
+
logger("channels").warn(`${key.channelId}:${key.conversationId} ${message}`);
|
|
152
|
+
void channels.notify(key.channelId, key.conversationId, message)
|
|
153
|
+
.catch((err) => log.error(`could not tell ${key.channelId} about its re-routed session`, err));
|
|
154
|
+
});
|
|
129
155
|
// Channels connect once tokens are readable; a refused unlock must not take
|
|
130
156
|
// down the web surface, which is where the operator repairs it. The chats a
|
|
131
157
|
// previous restart cut off are told as soon as channels are up (drain.ts).
|
|
@@ -248,10 +274,9 @@ registerConfigSyncRoutes(app, {
|
|
|
248
274
|
run: configurationSync.run,
|
|
249
275
|
});
|
|
250
276
|
registerTaskRoutes(app, tasks, { factory, router });
|
|
251
|
-
registerChannelRoutes(app, channelStore, channels);
|
|
277
|
+
registerChannelRoutes(app, channelStore, channels, handoff);
|
|
252
278
|
registerVaultRoutes(app, { vault, doctor: () => secrets.doctor() });
|
|
253
279
|
registerBoardRoutes(app);
|
|
254
|
-
const sessionState = new SessionStateStore(db);
|
|
255
280
|
registerPushRoutes(app, {
|
|
256
281
|
store: new PushStore(db, secrets),
|
|
257
282
|
hub,
|
|
@@ -279,7 +304,7 @@ app.route("/", createServer({
|
|
|
279
304
|
names: MANAGED.map((tool) => tool.name),
|
|
280
305
|
onToolsChanged: toolsUpdate.changed,
|
|
281
306
|
validateCustomTools: (raw) => {
|
|
282
|
-
const validated = normalizeCustomTools(raw
|
|
307
|
+
const validated = normalizeCustomTools(raw);
|
|
283
308
|
return validated ? { tools: validated } : { error: CUSTOM_TOOL_RULES };
|
|
284
309
|
},
|
|
285
310
|
secrets,
|
|
@@ -290,7 +315,7 @@ app.route("/", createServer({
|
|
|
290
315
|
backgroundRuns: (id) => tasks.backgroundRuns(id),
|
|
291
316
|
activeBackgroundRunCounts: () => tasks.activeBackgroundRunCounts(),
|
|
292
317
|
taskSessions: () => tasks.taskSessions(),
|
|
293
|
-
channelOf: (id) => conversations.
|
|
318
|
+
channelOf: (id) => conversations.keyOf(id)?.channelId,
|
|
294
319
|
}));
|
|
295
320
|
const port = Number(process.env.PORT ?? 3141);
|
|
296
321
|
const hostname = process.env.HOST ?? "127.0.0.1";
|
|
@@ -300,6 +325,14 @@ servePier({
|
|
|
300
325
|
// no public URL is set, since nothing in the process can discover one.
|
|
301
326
|
fileUrl: (name) => `${settings.get().publicUrl || `http://127.0.0.1:${String(port)}`}/#/settings/vault?name=${name}`,
|
|
302
327
|
task: (params, callerSessionId) => tasks.handle(params, callerSessionId),
|
|
328
|
+
// Progress and cost go to the log: the CLI's answer is the text alone.
|
|
329
|
+
web: async (params, callerSessionId) => {
|
|
330
|
+
const parsed = parseWebParams(params);
|
|
331
|
+
const note = (text) => log.info(`web ${callerSessionId}: ${text}`);
|
|
332
|
+
const result = await runWeb(parsed, await factory.webContext(router.modelOf(callerSessionId)), note);
|
|
333
|
+
note(`done ${JSON.stringify(result.details)}`);
|
|
334
|
+
return result;
|
|
335
|
+
},
|
|
303
336
|
// Live in the router, or on disk: the same two places a callback target is looked for.
|
|
304
337
|
knows: async (id) => router.stateOf(id) !== undefined || (await factory.find(id)) !== undefined,
|
|
305
338
|
});
|
package/dist/settings.js
CHANGED
|
@@ -63,12 +63,8 @@ export function normalizeModelRef(raw) {
|
|
|
63
63
|
return null;
|
|
64
64
|
return { provider: provider.trim(), id: id.trim() };
|
|
65
65
|
}
|
|
66
|
-
/** Shape only:
|
|
67
|
-
*
|
|
68
|
-
export function normalizeExtensions(raw) {
|
|
69
|
-
return normalizeNames(raw);
|
|
70
|
-
}
|
|
71
|
-
/** Shape only, for the same reason: tools.ts owns the catalog. */
|
|
66
|
+
/** Shape only: tools.ts owns the catalog, and an unknown name is ignored
|
|
67
|
+
* there, so a downgrade cannot lose a setting it cannot explain. */
|
|
72
68
|
export function normalizeTools(raw) {
|
|
73
69
|
return normalizeNames(raw);
|
|
74
70
|
}
|
|
@@ -98,11 +94,10 @@ export class SettingsStore {
|
|
|
98
94
|
modelMenu: this.#json("modelMenu", normalizeModelMenu, "a valid menu") ?? [],
|
|
99
95
|
...(titleModel ? { titleModel } : {}),
|
|
100
96
|
autoUpdate: this.#value("autoUpdate") === "1",
|
|
101
|
-
extensions: this.#json("extensions", normalizeExtensions, "a list of names") ?? [],
|
|
102
97
|
skillsOff: this.#json("skillsOff", normalizeNames, "a list of names") ?? [],
|
|
103
98
|
tools: this.#json("tools", normalizeTools, "a list of names") ?? [],
|
|
104
99
|
// `"drop"`: a row the bundled catalog has since taken is redundant, not malformed.
|
|
105
|
-
customTools: this.#json("customTools", (raw) => normalizeCustomTools(raw,
|
|
100
|
+
customTools: this.#json("customTools", (raw) => normalizeCustomTools(raw, "drop"), "a list of {name, toml}") ?? [],
|
|
106
101
|
};
|
|
107
102
|
}
|
|
108
103
|
/** A malformed row is named, not silently served as the empty value (§5). */
|
|
@@ -141,10 +136,6 @@ export class SettingsStore {
|
|
|
141
136
|
this.#set("autoUpdate", on ? "1" : "0");
|
|
142
137
|
return this.get();
|
|
143
138
|
}
|
|
144
|
-
setExtensions(names) {
|
|
145
|
-
this.#set("extensions", JSON.stringify(names));
|
|
146
|
-
return this.get();
|
|
147
|
-
}
|
|
148
139
|
setSkillsOff(names) {
|
|
149
140
|
this.#set("skillsOff", JSON.stringify(names));
|
|
150
141
|
return this.get();
|
package/dist/socket.js
CHANGED
|
@@ -28,17 +28,21 @@ const ROUTES = {
|
|
|
28
28
|
answer(500, { error: String(err) });
|
|
29
29
|
}
|
|
30
30
|
},
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
31
|
+
"/task": operation("task"),
|
|
32
|
+
"/web": operation("web"),
|
|
33
|
+
};
|
|
34
|
+
/** A CLI verb's params under the caller's session. 422, not 400: the request
|
|
35
|
+
* was well-formed; what the operation refused is the caller's to read. */
|
|
36
|
+
function operation(name) {
|
|
37
|
+
return async (hosts, { params }, sessionId, answer) => {
|
|
34
38
|
try {
|
|
35
|
-
answer(200, { result: await
|
|
39
|
+
answer(200, { result: await hosts[name](params, sessionId) });
|
|
36
40
|
}
|
|
37
41
|
catch (err) {
|
|
38
42
|
answer(422, { error: err instanceof Error ? err.message : String(err) });
|
|
39
43
|
}
|
|
40
|
-
}
|
|
41
|
-
}
|
|
44
|
+
};
|
|
45
|
+
}
|
|
42
46
|
export function servePier(hosts, path = PIER_SOCK) {
|
|
43
47
|
const server = createServer((req, res) => void handle(hosts, req, res).catch((err) => {
|
|
44
48
|
log.error(`${req.method ?? "?"} ${req.url ?? "?"} failed`, err);
|
package/dist/tasks/cli.js
CHANGED
|
@@ -17,10 +17,10 @@ const OPTIONS = {
|
|
|
17
17
|
* command accepts are read off it, so the two cannot drift. */
|
|
18
18
|
const COMMANDS = {
|
|
19
19
|
run: {
|
|
20
|
-
usage: "run [--prompt <text|->] [--run <id> [--after]] [--task-id <id>] [--session <id>]\n" +
|
|
20
|
+
usage: "run [--prompt <text|-> | --bash <script>] [--run <id> [--after]] [--task-id <id>] [--session <id>]\n" +
|
|
21
21
|
" [--model <name|?>] [--thinking <level>] [--cwd <dir>] [--name <text>] [--timeout <seconds>]\n" +
|
|
22
22
|
" [--callback origin|none|steer] [--callback-session <id>] [--join all|first] [--member <flags…>]…",
|
|
23
|
-
help: "a
|
|
23
|
+
help: "a new run (--prompt | --bash | --task-id | --session … --prompt), a batch (--member), or a prompt on an existing one (--run)",
|
|
24
24
|
},
|
|
25
25
|
save: {
|
|
26
26
|
usage: "save [--task-id <id>] --name <text> (--prompt <text|-> | --bash <script>)\n" +
|
|
@@ -35,7 +35,7 @@ const COMMANDS = {
|
|
|
35
35
|
const USAGE = [
|
|
36
36
|
"usage: pier task <command> … — subagents and scheduled tasks (skills/pier-tasks)",
|
|
37
37
|
...Object.values(COMMANDS).map(({ usage, help }) => ` ${usage}\n ${help}`),
|
|
38
|
-
"`--prompt -` reads stdin. The receipt is one line of JSON, exit 0; a refusal is one `task:` line, exit 1.",
|
|
38
|
+
"`--prompt -` reads stdin. The receipt is one line of JSON (`--model ?` one pin per line), exit 0; a refusal is one `task:` line, exit 1.",
|
|
39
39
|
].join("\n");
|
|
40
40
|
/** Flags that belong to the batch, never to one member. */
|
|
41
41
|
const GROUP_FLAGS = ["join", "callback", "callback-session"];
|
|
@@ -103,8 +103,9 @@ export async function runTaskCli(argv, post, io = processIo) {
|
|
|
103
103
|
}
|
|
104
104
|
const { status, body } = await post(params);
|
|
105
105
|
if (status === 200) {
|
|
106
|
-
// Compact: the reader is a model, and the ids are what it keeps
|
|
107
|
-
|
|
106
|
+
// Compact: the reader is a model, and the ids are what it keeps; an answer
|
|
107
|
+
// the server already wrote in lines (`--model ?`) is printed as it came.
|
|
108
|
+
io.stdout(typeof body.result === "string" ? body.result : JSON.stringify(body.result));
|
|
108
109
|
return 0;
|
|
109
110
|
}
|
|
110
111
|
io.stderr(`task: ${body.error ?? `socket answered ${String(status)}`}`);
|
|
@@ -141,12 +142,21 @@ function build(name, parsed, io) {
|
|
|
141
142
|
const launch = launchOf(v);
|
|
142
143
|
const timeoutSeconds = seconds("timeout", v.timeout);
|
|
143
144
|
if (v["task-id"] !== undefined) {
|
|
144
|
-
const extra = flagsOf(own).find((flag) => ["prompt", "session", "cwd", "model", "thinking", "name", "timeout"].includes(flag));
|
|
145
|
+
const extra = flagsOf(own).find((flag) => ["prompt", "bash", "session", "cwd", "model", "thinking", "name", "timeout"].includes(flag));
|
|
145
146
|
if (extra)
|
|
146
147
|
refuse(`--${extra} does not apply to a saved definition (--task-id)`);
|
|
147
148
|
return { task_id: v["task-id"] };
|
|
148
149
|
}
|
|
149
|
-
|
|
150
|
+
if (v.bash !== undefined) {
|
|
151
|
+
if (v.prompt !== undefined)
|
|
152
|
+
refuse("a run takes exactly one of --prompt or --bash");
|
|
153
|
+
if (launch)
|
|
154
|
+
refuse("--model/--thinking apply to a prompt, not --bash");
|
|
155
|
+
if (v.session !== undefined)
|
|
156
|
+
refuse("--session continues an agent session; --bash is a command, not a turn");
|
|
157
|
+
return compact({ name: v.name, timeoutSeconds, action: compact({ type: "bash", script: v.bash, cwd: v.cwd }) });
|
|
158
|
+
}
|
|
159
|
+
const prompt = text(v.prompt) ?? refuse("a new run needs --prompt, --bash or --task-id");
|
|
150
160
|
if (v.session === undefined)
|
|
151
161
|
return compact({ prompt, cwd: v.cwd, launch, name: v.name, timeoutSeconds });
|
|
152
162
|
if (v.cwd !== undefined)
|
package/dist/tasks/operations.js
CHANGED
|
@@ -129,8 +129,11 @@ export async function handleTask(host, definitions, store, raw, callerSessionId)
|
|
|
129
129
|
}
|
|
130
130
|
if (input.operation === "run") {
|
|
131
131
|
// `--model ?`: the menu instead of a run, the one lookup the common case never pays.
|
|
132
|
-
if (record(input.launch)?.model === "?")
|
|
133
|
-
|
|
132
|
+
if (record(input.launch)?.model === "?") {
|
|
133
|
+
const { source, models } = await host.models();
|
|
134
|
+
const head = source === "menu" ? "the operator's menu" : "the live catalog (no model pinned)";
|
|
135
|
+
return `${head} — --model takes a provider/id or a unique substring of one:\n${menuLines(models)}`;
|
|
136
|
+
}
|
|
134
137
|
const callbackMode = callbackModeOf(input);
|
|
135
138
|
if (Array.isArray(input.tasks)) {
|
|
136
139
|
// Core-joined fan-out: members run detached, one aggregated callback.
|
|
@@ -245,8 +248,8 @@ function inlineDraft(input) {
|
|
|
245
248
|
return { prompt, cwd, launch, name, timeoutSeconds };
|
|
246
249
|
}
|
|
247
250
|
/** A label for the Console, not an identifier. */
|
|
248
|
-
function
|
|
249
|
-
const line =
|
|
251
|
+
function nameFrom(text) {
|
|
252
|
+
const line = text.split("\n")
|
|
250
253
|
.map((l) => l.replace(/^[\s#>*-]+/, "").replace(/[*_`]/g, "").replace(/\s+/g, " ").trim())
|
|
251
254
|
.find(Boolean) ?? "subagent";
|
|
252
255
|
return line.length > 60 ? `${line.slice(0, 59).trimEnd()}…` : line;
|
|
@@ -257,7 +260,6 @@ function nameFromPrompt(prompt) {
|
|
|
257
260
|
function resolveModel(name, menu) {
|
|
258
261
|
const needle = name.trim().toLowerCase();
|
|
259
262
|
const full = (pin) => `${pin.provider}/${pin.id}`;
|
|
260
|
-
const line = (pin) => `${full(pin)}${pin.thinking ? ` · ${pin.thinking}` : ""}${pin.note ? ` — ${pin.note}` : ""}`;
|
|
261
263
|
const exact = menu.find((pin) => full(pin).toLowerCase() === needle);
|
|
262
264
|
const hits = exact ? [exact] : menu.filter((pin) => `${full(pin)} ${pin.note ?? ""}`.toLowerCase().includes(needle));
|
|
263
265
|
if (hits.length === 1)
|
|
@@ -265,9 +267,11 @@ function resolveModel(name, menu) {
|
|
|
265
267
|
const slash = name.indexOf("/");
|
|
266
268
|
if (!hits.length && slash > 0 && slash < name.length - 1)
|
|
267
269
|
return { model: { provider: name.slice(0, slash), id: name.slice(slash + 1) } };
|
|
268
|
-
|
|
269
|
-
throw new Error(`model "${name}" matches ${String(hits.length)} of the menu:\n${lines}`);
|
|
270
|
+
throw new Error(`model "${name}" matches ${String(hits.length)} of the menu:\n${menuLines(hits.length ? hits : menu)}`);
|
|
270
271
|
}
|
|
272
|
+
/** One pin per line, the only shape a menu is ever printed in. */
|
|
273
|
+
const menuLines = (menu) => menu.map((pin) => `${pin.provider}/${pin.id}${pin.thinking ? ` · ${pin.thinking}` : ""}${pin.note ? ` — ${pin.note}` : ""}`).join("\n")
|
|
274
|
+
|| "(no model is pinned or available)";
|
|
271
275
|
/** A `prompt` shorthand becomes a fresh Agent action in the caller's own
|
|
272
276
|
* directory; everything the caller did spell out passes through to parseDraft. */
|
|
273
277
|
async function expandDraft(definitions, menu, raw, callerSessionId) {
|
|
@@ -282,14 +286,22 @@ async function expandDraft(definitions, menu, raw, callerSessionId) {
|
|
|
282
286
|
}
|
|
283
287
|
const action = record(draft.action);
|
|
284
288
|
const session = record(action?.session);
|
|
285
|
-
|
|
289
|
+
const absolute = async (cwd) => {
|
|
286
290
|
const base = await definitions.sessionCwd(callerSessionId);
|
|
287
291
|
if (!base)
|
|
288
|
-
throw new Error(`cwd ${
|
|
289
|
-
|
|
292
|
+
throw new Error(`cwd ${cwd === undefined ? "omitted" : `"${String(cwd)}" is relative`} and the calling session has no working directory; give an absolute path`);
|
|
293
|
+
return resolve(base, typeof cwd === "string" ? cwd : ".");
|
|
294
|
+
};
|
|
295
|
+
const relative = (cwd) => cwd === undefined || (typeof cwd === "string" && !isAbsolute(cwd));
|
|
296
|
+
if (action?.type === "agent" && session?.mode === "fresh" && relative(session.cwd)) {
|
|
297
|
+
draft = { ...draft, action: { ...action, session: { ...session, cwd: await absolute(session.cwd) } } };
|
|
298
|
+
}
|
|
299
|
+
if (action?.type === "bash" && relative(action.cwd)) {
|
|
300
|
+
draft = { ...draft, action: { ...action, cwd: await absolute(action.cwd) } };
|
|
290
301
|
}
|
|
291
|
-
|
|
292
|
-
|
|
302
|
+
const label = action?.prompt ?? action?.script;
|
|
303
|
+
if (draft.name === undefined && typeof label === "string")
|
|
304
|
+
draft = { ...draft, name: nameFrom(label) };
|
|
293
305
|
const launch = record(action?.launch);
|
|
294
306
|
if (typeof launch?.model === "string") {
|
|
295
307
|
const { model, thinking } = resolveModel(launch.model, await menu());
|
package/dist/tools.js
CHANGED
|
@@ -80,14 +80,12 @@ export const CUSTOM_TOOL_RULES = `each custom tool needs a name (letters, digits
|
|
|
80
80
|
* `install_dir` anywhere, and a managed name twice is a switch whose meaning
|
|
81
81
|
* depends on which ran last. Key validity is ubix's to report. */
|
|
82
82
|
export function normalizeCustomTools(raw,
|
|
83
|
-
/** Names this file cannot see: the bundled extensions, handed in by main.ts. */
|
|
84
|
-
reserved = [],
|
|
85
83
|
/** `"drop"` when reading a stored row: the catalog may have grown into that
|
|
86
84
|
* name after it was written, and the bundled row installs the same binary.
|
|
87
85
|
* The declaration stays in the row for a Pier that stops bundling it. */
|
|
88
86
|
managedName = "reject") {
|
|
89
87
|
// Case-insensitively: one filename on a case-insensitive filesystem.
|
|
90
|
-
const taken = new Set([...MANAGED.map((tool) => tool.name),
|
|
88
|
+
const taken = new Set([...MANAGED.map((tool) => tool.name), "ubix"].map((name) => name.toLowerCase()));
|
|
91
89
|
if (!Array.isArray(raw) || raw.length > MAX_CUSTOM)
|
|
92
90
|
return null;
|
|
93
91
|
const tools = [];
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{E as e,G as t,H as n,J as r,K as i,M as a,R as o,S as s,Y as c,Z as l,nt as u}from"./index-
|
|
1
|
+
import{E as e,G as t,H as n,J as r,K as i,M as a,R as o,S as s,Y as c,Z as l,nt as u}from"./index-BrNHu2qj.js";var d=(e,t={})=>{let n=document.createElementNS(`http://www.w3.org/2000/svg`,e);for(let[e,r]of Object.entries(t))n.setAttribute(e,r);return n},f=e=>e===null?`-`:t(Date.now()-e),p={light:{edge:`#a3a3a3`,callback:`#0891b2`,message:`#d97706`,scheduler:{fill:`#f5f3ff`,stroke:`#a78bfa`,text:`#5b21b6`},console:{fill:`#eff6ff`,stroke:`#93c5fd`,text:`#1d4ed8`},task:{fill:`#fffbeb`,stroke:`#fbbf24`,text:`#92400e`},process:{fill:`#f5f5f5`,stroke:`#a3a3a3`,text:`#525252`,dash:`4 3`},streaming:{fill:`#ffffff`,stroke:`#10b981`,text:`#262626`},idle:{fill:`#fafafa`,stroke:`#d4d4d4`,text:`#737373`}},dark:{edge:`#7a7a7a`,callback:`#3fc3dd`,message:`#e0a13a`,scheduler:{fill:`#2b2440`,stroke:`#7d63c9`,text:`#c9b8f5`},console:{fill:`#1f2a3d`,stroke:`#4a7fbf`,text:`#a8c8f0`},task:{fill:`#3a2f1c`,stroke:`#b8862a`,text:`#f0d49a`},process:{fill:`#262626`,stroke:`#5c5c5c`,text:`#a3a3a3`,dash:`4 3`},streaming:{fill:`#22302a`,stroke:`#10b981`,text:`#e5e5e5`},idle:{fill:`#212121`,stroke:`#4a4a4a`,text:`#9a9a9a`}}},m=()=>p[document.documentElement.dataset.theme===`dark`?`dark`:`light`];function h(t,p,h){let g=`sessions`,_=`active`,v={sessions:[],runs:[],messages:[]},y=``,b=c(async()=>{let e=_,n=await l(`/api/activity?scope=${e}`,`Failed to load activity`);if(!n.ok){y=``,t.replaceChildren(i(`p`,`p-4 text-[13px] text-red-600`,n.error));return}if(e!==_)return;let r=JSON.stringify(n.value);if(`${e}:${r}`===y)return;y=`${e}:${r}`;let a=n.value;v={...a,sessions:a.sessions.map(e=>({...e,title:u(e.title)}))},x()});function x(){let e=o(a([[`Sessions`,`sessions`],[`Relationships`,`dependencies`]],g,e=>{g=e,x()}),i(`span`,`ml-auto text-[11.5px] text-neutral-400`,`${v.sessions.length} sessions · ${v.runs.length} runs`),a([[`Current`,`active`],[`Last 24h`,`recent`]],_,e=>{_=e,x(),b()})),n=i(`div`,`automation-list min-h-0 flex-1 overflow-auto`);g===`sessions`?S(n):w(n),t.replaceChildren(e,n)}function S(t){let n=document.createElement(`table`);n.className=`w-full table-fixed text-left text-[12.5px]`,n.innerHTML=`<thead class="sticky top-0 bg-neutral-50 text-[10.5px] uppercase tracking-wide text-neutral-400 shadow-[inset_0_-1px_0_var(--color-neutral-200)]"><tr>
|
|
2
2
|
<th class="w-[50%] px-4 py-2 font-semibold md:w-[34%]">Session</th>
|
|
3
3
|
<th class="hidden w-[38%] px-2 py-2 font-semibold md:table-cell">Project</th>
|
|
4
4
|
<th class="px-2 py-2 font-semibold md:w-[14%]">State</th>
|
|
Binary file
|
|
Binary file
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{A as e,C as t,E as n,H as r,K as i,L as a,T as o,U as s,X as c,Z as l,et as u,q as d,tt as f}from"./index-
|
|
1
|
+
import{A as e,C as t,E as n,H as r,K as i,L as a,T as o,U as s,X as c,Z as l,et as u,q as d,tt as f}from"./index-BrNHu2qj.js";var p=e=>e.public&&e.token?`/p/${e.slug}-${e.token}/`:`/boards/${e.slug}/`;function m(m,h){let g=[],_=``,v=i(`header`,`pagehead sticky top-[-8px] z-30 max-md:hidden`,e(`Boards`)),y=i(`div`,`px-4 pb-5 pt-1`);m.append(i(`div`,`min-h-0 flex-1 overflow-y-auto`,v,y));async function b(e,t){let n=await f(`/api/boards/${e.slug}`,{public:t},`PATCH`);if(n.ok)return e.token=(await n.json().catch(()=>({}))).token??``,w();_=await c(n,`Could not change ${e.slug}`),await T()}async function x(e){_=await u(`/api/boards/${e}`,`DELETE`,`Could not delete ${e}`)??``,await T()}function S(e){let n=i(`div`,`group flex flex-col gap-1 py-2.5`),r=document.createElement(`a`);r.className=`min-w-0 flex-1 truncate text-[13px] font-medium text-neutral-800 hover:text-indigo-700 max-md:basis-full`,r.href=p(e),r.target=`_blank`,r.rel=`noreferrer`,r.textContent=e.title;let o=i(`div`,`flex flex-wrap items-center gap-x-2 gap-y-1`,r),c=a(``,``,e.public,t=>{e.public=t,b(e,t)});c.className=`flex flex-none cursor-pointer items-center gap-1.5 text-[11.5px] text-neutral-500`,c.append(i(`span`,``,`Public`));let l=t(`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`);l.title=`Renames the folder on disk; nothing is erased`,l.onclick=()=>void x(e.slug);let u=s(`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)}`);u.title=`Copy the board's link`,o.append(i(`div`,`ml-auto flex flex-none items-center gap-2`,c,u,l)),n.append(o);let f=i(`div`,`flex flex-wrap items-center gap-x-3 gap-y-1 text-[11.5px] text-neutral-500`);f.append(i(`span`,`min-w-0 break-all font-mono text-neutral-400`,e.slug)),e.description&&f.append(i(`span`,`min-w-0 truncate`,e.description)),f.append(i(`span`,`flex-none text-neutral-400`,d(Date.parse(e.updatedAt))));for(let n of e.sessions){let e=t(n.slice(0,8),`flex-none cursor-pointer font-mono text-indigo-600 hover:underline`);e.title=`Open session ${n}`,e.onclick=()=>h(n),f.append(e)}if(e.public&&e.token){let t=i(`span`,`min-w-0 break-all font-mono text-neutral-400`,p(e));t.title=`Public URL — anyone holding this link can read the board, no password`,f.append(t)}return n.append(f),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=i(`div`,`mx-auto flex w-full max-w-3xl flex-col gap-4`);g.length===0&&e.append(n(`No boards yet. Ask an agent to build one — it writes a folder under ~/.pier/boards.`));for(let{title:t,hint:n,wanted:r}of C){let a=g.filter(e=>e.public===r);a.length!==0&&e.append(o(t,n,i(`div`,`flex flex-col divide-y divide-neutral-200/70`,...a.map(S))))}y.replaceChildren(..._?[i(`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=r(m,()=>void T());return window.addEventListener(`focus`,()=>{E.visible&&T()}),E}export{m as createBoardsView};
|
|
Binary file
|
|
Binary file
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{A as e,C as t,E as n,H as r,K as i,Q as a,V as o,W as s,_ as c,a as l,at as u,dt as d,f,h as p,i as m,it as h,l as g,m as ee,p as te,q as _,s as ne,ut as re,v as ie}from"./index-
|
|
1
|
+
import{A as e,C as t,E as n,H as r,K as i,Q as a,V as o,W as s,_ as c,a as l,at as u,dt as d,f,h as p,i as m,it as h,l as g,m as ee,p as te,q as _,s as ne,ut as re,v as ie}from"./index-BrNHu2qj.js";var ae=[[`path`,{d:`M12 5v14`}],[`path`,{d:`m19 12-7 7-7-7`}]],oe=[[`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`}]],se=[[`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`}]],ce=[[`path`,{d:`M15 6a9 9 0 0 0-9 9V3`}],[`circle`,{cx:`18`,cy:`6`,r:`3`}],[`circle`,{cx:`6`,cy:`18`,r:`3`}]],v=[[`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`}]],y=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())}`},le=e=>[e.subject,e.body,[[e.author,e.email?`<${e.email}>`:``].filter(Boolean).join(` `),e.at?y(e.at):``].filter(Boolean).join(` · `)].filter(Boolean).join(`
|
|
2
2
|
|
|
3
3
|
`),b=null;function x(){b&&(b.remove(),b=null,document.removeEventListener(`pointerdown`,x,!0),window.removeEventListener(`scroll`,x,!0))}function ue(e,t){e.onmouseenter=()=>{x();let n=t();if(!n)return;b=i(`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`),b.textContent=n,document.body.append(b),document.addEventListener(`pointerdown`,x,!0),window.addEventListener(`scroll`,x,!0);let r=e.getBoundingClientRect();b.style.left=`${Math.max(8,Math.min(r.left,window.innerWidth-b.offsetWidth-8))}px`,b.style.top=r.bottom+6+b.offsetHeight>window.innerHeight?`${r.top-b.offsetHeight-6}px`:`${r.bottom+6}px`},e.onmouseleave=x}function S(e,t,n){let r=i(`button`,`flex w-full cursor-pointer items-center gap-1.5 px-2 py-1 text-left hover:bg-neutral-100`,h(re,`h-3 w-3 text-indigo-600 ${e.value===t?``:`invisible`}`),i(`span`,`inline-flex flex-none items-center gap-1.5 font-mono text-[11.5px] text-neutral-800`,e.label??e.value));return e.subject&&r.append(i(`span`,`min-w-0 truncate text-[11px] text-neutral-400`,e.subject)),e.author&&r.append(i(`span`,`ml-auto flex-none pl-1.5 text-[10.5px] text-neutral-400`,`${e.author} · ${e.at?_(e.at):``}`)),ue(r,()=>le(e)),r.onclick=()=>n(e.value),r}function de(e,t,n,r,a){let{base:o,head:s}=n,c=()=>s&&o===`${s}~1`?s:``,u=c()?`commit`:`compare`,d=t.commits[0],f=e=>[...e?[{value:``,label:`Working tree`,subject:`the files on disk, uncommitted changes included`}]:[],{...d,value:`HEAD`},...t.refs.map(e=>({value:e.name,subject:e.subject})),...t.commits.map(e=>({...e,value:e.hash}))],p=i(`div`,`flex w-[38rem] max-w-full flex-col`);function m(){let e=(e,t)=>{let n=i(`button`,`cursor-pointer rounded-full px-2.5 py-0.5 text-[11.5px] ${u===t?`bg-indigo-50 font-medium text-indigo-700`:`text-neutral-500 hover:bg-neutral-100`}`,e);return n.onclick=()=>{u=t,m()},n},n=(e,t,n,r)=>i(`div`,`flex min-w-0 flex-1 flex-col`,i(`div`,`flex-none px-2 pb-0.5 text-[10.5px] font-semibold uppercase tracking-wide text-neutral-400`,e),i(`div`,`min-h-0 flex-1 overflow-y-auto`,...t.map(e=>S(e,n,r)))),d=u===`compare`?i(`div`,`flex h-72 min-h-0 divide-x divide-neutral-200 pb-1`,n(`base`,f(!1),o,e=>{o=e,m(),r(o,s)}),n(`to`,f(!0),s,e=>{s=e,m(),r(o,s)})):i(`div`,`flex h-72 min-h-0 flex-col overflow-y-auto pb-1`,...t.commits.map(e=>S({value:e.hash,...e},c(),e=>{o=`${e}~1`,s=e,l(),r(o,s)}))),g=i(`div`,`flex-none border-t border-neutral-200`,S({value:a,label:`${a} ↔ Working tree`,subject:`back to the default: the main line's tip vs the files on disk`},o===a&&s===``?a:``,()=>{o=a,s=``,m(),r(o,s)}));g.querySelector(`button > span`).prepend(h(v)),p.replaceChildren(i(`div`,`flex flex-none items-center gap-1 border-b border-neutral-200 px-2 py-1.5`,e(`Compare two points`,`compare`),e(`One commit`,`commit`)),d,...u===`compare`?[g]:[])}m(),ne(e,p)}var fe=/\.(png|jpe?g|gif|webp|avif|bmp|svg)$/i,C={A:`text-emerald-600`,M:`text-amber-600`,D:`text-red-600`},pe=30,w=`pier.filesPrefs`,T=50,E=()=>{try{let e=JSON.parse(localStorage.getItem(w)??`{}`);return e&&typeof e==`object`?e:{}}catch{return{}}},me=e=>{let t=e?E()[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},he=(e,t)=>{let n=E();delete n[e],n[e]=t;let r=Object.keys(n);for(let e of r.slice(0,Math.max(0,r.length-T)))delete n[e];try{localStorage.setItem(w,JSON.stringify(n))}catch(e){console.warn(`Files: could not remember this folder`,e)}},ge=`flex min-w-0 flex-none items-center gap-1.5 rounded-full px-2.5 py-1 font-mono text-[11.5px]`,_e=(e,n=`indigo`)=>t(e,`${ge} cursor-pointer truncate text-left ${n===`indigo`?`bg-indigo-50 font-medium text-indigo-700 hover:bg-indigo-100`:`bg-neutral-100 text-neutral-600 hover:bg-neutral-200`}`);function D(l,_,ne,re){let v=``,y=``,b={branch:null,refs:[],commits:[],worktrees:[]},x=`HEAD`,S=``,w=new Map,T=!0,E=!1,D=[],O=new Set,k=null,A=null,j=null,ve=i(`header`,`pagehead flex-nowrap`),M=i(`div`,`${p} flex-col items-stretch gap-1.5 px-3 py-2.5`),N=i(`div`,`min-h-0 flex-1 overflow-y-auto px-1.5 py-1.5`),ye=i(`aside`,`${ie} w-64 flex-none text-[12.5px] leading-5 max-md:h-1/2 max-md:w-full`,M,N),P=i(`div`,`${p} flex-wrap gap-2 px-4 py-2.5 font-mono text-[11.5px] text-neutral-500`),F=i(`div`,`h-full min-w-0 overflow-auto`),I=i(`div`,`pointer-events-none absolute inset-y-1 right-0.5 z-20 hidden w-2`),be=i(`div`,`relative min-h-0 flex-1`,F,I),xe=i(`section`,`${ie} min-h-0 min-w-0 flex-1`,P,be);l.append(ve,i(`div`,`flex min-h-0 flex-1 gap-3 px-4 pb-4 pt-1 max-md:flex-col`,ye,xe));let L=(e,t)=>`/api/explorer/${e}?${new URLSearchParams({root:v,...t})}`,R=(e,t)=>`/api/fs/${e}?${new URLSearchParams({root:v,...t})}`,z=(e,t=`text-neutral-400`)=>i(`p`,`px-3 py-2 text-[12.5px] ${t}`,e),B=e=>i(`div`,`flex h-full items-center justify-center p-6`,n(e)),Se=()=>T&&b.branch!==null,Ce=e=>{let t=0;for(let n of w.keys())n.startsWith(`${e}/`)&&t++;return t};function we(e,t){let n=new Set(t.map(e=>e.name)),r=e?`${e}/`:``;for(let[e,i]of w){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 Se()&&(i=t.filter(e=>e.dir?Ce(`${r}${e.name}`)>0:w.has(`${r}${e.name}`))),i.sort((e,t)=>Number(t.dir)-Number(e.dir)||e.name.localeCompare(t.name)),i.map(e=>e.dir?Ee(`${r}${e.name}`,e.name):De(`${r}${e.name}`,e.name))}async function Te(e,t){try{let{entries:n}=await a(R(`ls`,{path:t}),`could not list this folder`);e.replaceChildren(...we(t,n)),e.childElementCount||e.append(z(Se()?`No changes.`:`Empty.`))}catch(n){let r=we(t,[]);r.length?e.replaceChildren(...r):e.replaceChildren(z(String(n),`text-red-600`))}}function Ee(e,t){let n=Ce(e),r=[i(`span`,`truncate`,t)];n&&r.push(i(`span`,`ml-auto flex-none rounded-full bg-amber-100/80 px-1.5 font-mono text-[10px] font-semibold text-amber-700`,String(n)));let{el:a,summary:o}=s(``,r);o.className+=` rounded-lg px-2 py-1 transition-colors hover:bg-neutral-100`;let c=i(`div`,`pl-3`);a.append(c);let l=!1,u=()=>{l||(l=!0,c.append(z(`…`)),Te(c,e))};return a.ontoggle=()=>{O[a.open?`add`:`delete`](e),a.open&&u()},((b.branch?T:!e.includes(`/`))&&!E||O.has(e))&&(a.open=!0,u()),a}function V(e){A?.classList.remove(`bg-indigo-50`,`font-medium`,`text-indigo-700`),A=e,e.classList.add(`bg-indigo-50`,`font-medium`,`text-indigo-700`)}function De(e,t){let n=w.get(e)?.status,r=i(`button`,`flex w-full cursor-pointer items-center gap-1.5 rounded-lg py-1 pl-6 pr-2 text-left transition-colors hover:bg-neutral-100`,i(`span`,`truncate`,t));return n&&r.append(i(`span`,`ml-auto flex-none font-mono text-[10.5px] font-semibold ${C[n]??`text-neutral-500`}`,n)),r.title=e,e===k&&V(r),e===j&&(j=null,k=e,V(r),J(e)),r.onclick=()=>{V(r),k=e,J(e)},r}let H=()=>void Te(N,``),U=(e,t)=>[...e?[i(`span`,`flex-none font-mono text-[10.5px] font-semibold text-emerald-600`,`+${e}`)]:[],...t?[i(`span`,`flex-none font-mono text-[10.5px] font-semibold text-red-600`,`−${t}`)]:[]];function W(e,t=!1,n){if(P.classList.toggle(`hidden`,!e),P.classList.toggle(`flex`,!!e),!e)return P.replaceChildren();let r=w.get(e),a=[i(`span`,`min-w-0 truncate`,e)];r&&(a.push(i(`span`,`flex-none font-semibold ${C[r.status]??``}`,r.status)),a.push(...U(r.add,r.del)));let s=i(`div`,`ml-auto flex flex-none items-center gap-2`);if(n&&s.append(n),t){let t=document.createElement(`a`);t.className=`btn flex-none text-[12px]`,t.href=R(`file`,{path:e}),t.download=o(e),t.textContent=`Download`,s.append(t)}P.replaceChildren(...a,s)}function Oe(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 G=Ie();function K(){I.classList.add(`hidden`),I.replaceChildren(),G.set([],[])}let ke=0,q=e=>e===ke;function J(e){let t=++ke;return w.has(e)?Le(e,t):Ae(e,t)}async function Ae(e,t){if(!q(t))return;K();let n=R(`file`,{path:e}),r=i(`div`,`min-w-0`);if(F.classList.remove(`flex`,`flex-col`),W(e,!0),F.replaceChildren(r),fe.test(e)){let e=i(`img`,`max-w-full p-4`);e.src=n,e.onerror=()=>r.replaceChildren(z(`Could not load image.`,`text-red-600`)),r.append(e);return}if(e.toLowerCase().endsWith(`.pdf`)){let e=document.createElement(`iframe`);e.className=`h-full w-full`,e.src=n,F.replaceChildren(e),F.classList.add(`flex`,`flex-col`);return}r.append(z(`…`));let a=await fetch(n);if(!q(t))return;if(!a.ok){let{error:e}=await a.json().catch(()=>({}));r.replaceChildren(z(e??`HTTP ${a.status}`,`text-red-600`));return}if(!(a.headers.get(`content-type`)??``).startsWith(`text/plain`)){r.replaceChildren(z(`Binary file — use Download.`));return}let o=te(await a.text());if(!q(t))return;let s=await ee(e);q(t)&&r.replaceChildren(f(o,s))}function je(e){let t=[],n=0,r=0,i=!1,a=e.split(`
|
|
4
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 Me(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 Ne={add:`bg-emerald-400`,del:`bg-red-400`,mixed:`bg-amber-400`};function Pe(e,t,n){I.replaceChildren(...e.map(e=>{let r=i(`button`,`map-mark pointer-events-auto absolute left-1/2 w-1 -translate-x-1/2 cursor-pointer rounded-full ${Ne[e.tone]}`);return r.style.top=`${e.start/t*100}%`,r.style.height=`${Math.max(.4,(e.end-e.start+1)/t*100)}%`,r.title=`line ${Fe(n,e.start)}`,r.onclick=()=>n[e.start].scrollIntoView({block:`center`}),r})),I.classList.remove(`hidden`)}let Fe=(e,t)=>e[t]?.querySelector(`span`)?.textContent?.trim()||String(t+1);function Ie(){let e=[],t=[],n=-1,r=i(`span`,`flex-none text-[10.5px] text-neutral-400`),a=i=>{e.length&&(n=(n+i+e.length)%e.length,t[e[n].start].scrollIntoView({block:`center`}),r.textContent=`${n+1}/${e.length}`)},o=()=>e.length>0&&!l.classList.contains(`hidden`),s=(e,t,n,r)=>{let s=i(`button`,`icon-btn flex-none`,h(e));return s.onclick=()=>a(t),g(s,r,n,()=>a(t),o),s};return{el:i(`div`,`flex flex-none items-center gap-1`,r,s(d,-1,`Previous change`,[`p`,`k`]),s(ae,1,`Next change`,[`n`,`j`])),set(i,a){e=i,t=a,n=-1,r.textContent=`${i.length} change${i.length===1?``:`s`}`}}}async function Le(e,t){K(),F.classList.remove(`flex`,`flex-col`);let n=w.get(e)?.status!==`D`;W(e,n),F.replaceChildren(z(`…`));try{let{diff:r}=await a(L(`diff`,{base:x,head:S,file:e,context:`99999`}),`could not read this diff`);if(!q(t))return;if(!r||/^Binary files /m.test(r))return Ae(e,t);let i=je(r);Oe(i);let o=await ee(e);if(!q(t))return;let s=f(i,o),c=[...s.children],l=Me(i);G.set(l,c),W(e,n,G.el),F.replaceChildren(s),Pe(l,i.length,c)}catch(n){if(!q(t))return;W(e),F.replaceChildren(z(String(n),`text-red-600`))}}let Y=()=>S&&x===`${S}~1`?S:``,Re=()=>b.commits.find(e=>e.hash===Y())?.subject??``,ze=()=>Y()?`${Y()} — ${Re()||`commit`}`:`${x} ↔ ${S||`working tree`}`,Be=(e,t,n,r)=>{let a=i(`button`,`icon-btn ${n?`bg-indigo-50 text-indigo-700 hover:bg-indigo-100 hover:text-indigo-700`:``}`);return a.append(h(e)),a.title=t,a.setAttribute(`aria-label`,t),a.setAttribute(`aria-pressed`,String(n)),a.onclick=r,a};function X(){let e=Be(se,`Show only files that differ`,T,()=>{T=!T,X(),H()}),n=Be(oe,E?`Restore folders`:`Collapse all folders`,E,()=>{if(E=!E,E)D=[...O],O.clear();else for(let e of D)O.add(e);X(),H()});if(!b.branch){M.replaceChildren(i(`div`,`flex items-center gap-1.5`,i(`span`,`min-w-0 flex-1 text-[11px] leading-snug text-neutral-400`,`Not a git repository — nothing to compare.`),n));return}let r=t(ze(),`${c} font-mono`);if(Y()){let e=b.commits.find(e=>e.hash===Y());ue(r,()=>e?le(e):``)}else r.title=`Choose what to diff`;r.onclick=async()=>{await We(),de(r,b,{base:x,head:S},(e,t)=>{x=e,S=t,Z()},$())};let[a,o]=[0,0];for(let e of w.values())a+=e.add,o+=e.del;M.replaceChildren(r,i(`div`,`flex items-center gap-1.5 pl-0.5`,i(`span`,`text-[11px] text-neutral-400`,`${w.size} changed file${w.size===1?``:`s`}`),...U(a,o),i(`span`,`ml-auto`),e,n))}let Ve=()=>{y&&v&&he(y,{cwd:v,base:x,head:S})},He=e=>{let t=e.replace(/~\d+$/,``);return!t||t===`HEAD`||b.refs.some(e=>e.name===t)||b.commits.some(e=>e.hash===t)};async function Ue(){if(w=new Map,!b.branch)return;let{files:e}=await a(L(`diff`,{base:x,head:S}),`could not read what changed`);for(let t of e)w.set(t.path,{status:t.status,add:t.add,del:t.del});if(w.size<=pe)for(let e of w.keys()){let t=e.split(`/`);for(let e=1;e<t.length;e++)O.add(t.slice(0,e).join(`/`))}}async function Z(){Ve();try{await Ue()}catch(e){X(),N.replaceChildren(z(String(e),`text-red-600`));return}X(),H(),k&&J(k)}function Q(){let t=_e(``,`neutral`);t.className+=` max-w-72`,t.append(i(`span`,`truncate max-md:hidden`,v||`Choose a folder…`),i(`span`,`truncate md:hidden`,v?o(v):`Choose a folder…`)),t.title=v?`${v} — switch folder`:`Choose a folder`,t.onclick=()=>m(t,b.worktrees.map(e=>({path:e.path,...e.branch?{hint:e.branch}:{}})),v||void 0,ne);let n=i(`button`,`icon-btn ml-auto`,h(u));n.type=`button`,n.title=`Close Files`,n.setAttribute(`aria-label`,`Close Files`),n.onclick=re;let r=b.branch?[i(`span`,`${ge} bg-neutral-100 text-neutral-600`,h(ce),i(`span`,`truncate`,b.branch))]:[];ve.replaceChildren(e(`Files`),t,...r,n)}async function We(){if(v)try{b=await a(L(`git`,{}),`could not read this repository`),Q()}catch(e){F.replaceChildren(B(String(e)))}}let $=()=>b.refs.some(e=>e.name===`main`)?`main`:b.refs.some(e=>e.name===`master`)?`master`:`HEAD`;async function Ge(e){if(Q(),k=null,A=null,O.clear(),j=e||null,e){T=!1;let t=e.split(`/`);for(let e=1;e<t.length;e++)O.add(t.slice(0,e).join(`/`))}if(K(),F.classList.remove(`flex`,`flex-col`),W(),F.replaceChildren(B(`Select a file.`)),!v){M.replaceChildren(),N.replaceChildren(),F.replaceChildren(B(`No folder yet — pick one from the chip above.`));return}try{b=await a(L(`git`,{}),`could not read this repository`)}catch(e){b={branch:null,refs:[],commits:[],worktrees:[]},F.replaceChildren(B(String(e)))}let t=me(y),n=t&&t.cwd===v&&He(t.base)&&He(t.head)?t:null;x=n?.base??$(),S=n?.head??``,Q(),await Z()}return r(l,(e,t)=>{let n=_(),r=n?.id??``,i=new URLSearchParams(t).get(`select`)??void 0,a=e?.startsWith(`/`)?e:(r===y?v:``)||me(r)?.cwd||n?.cwd||``;if(a===v&&r===y&&N.childElementCount&&(!i||i===k)){Q(),We().then(Z);return}y=r,v=a,Ge(i)},()=>{K(),F.classList.remove(`flex`,`flex-col`),W(),F.replaceChildren(B(`Select a file.`))})}export{D as createExplorerView};
|
|
Binary file
|
|
Binary file
|