allaboard 0.0.0-stage → 0.5.0
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/LICENSE +21 -0
- package/README.md +43 -2
- package/dist/agent/claude/backend.js +124 -0
- package/dist/agent/claude/capabilities.js +30 -0
- package/dist/agent/claude/chrome.js +42 -0
- package/dist/agent/claude/hooks.js +37 -0
- package/dist/agent/claude/info.js +61 -0
- package/dist/agent/claude/listing.js +230 -0
- package/dist/agent/claude/module.js +82 -0
- package/dist/agent/claude/prompt.js +24 -0
- package/dist/agent/claude/record.js +9 -0
- package/dist/agent/claude/requests.js +122 -0
- package/dist/agent/claude/session.js +441 -0
- package/dist/agent/claude/translate.js +581 -0
- package/dist/agent/claude/trust.js +576 -0
- package/dist/agent/claude/usage-probe.js +103 -0
- package/dist/agent/claude/usage.js +110 -0
- package/dist/agent/claude/wire.js +22 -0
- package/dist/agent/seam.js +21 -0
- package/dist/chat/seam.js +10 -0
- package/dist/chat/slack/app/answers.js +209 -0
- package/dist/chat/slack/app/app.js +262 -0
- package/dist/chat/slack/app/clip.js +191 -0
- package/dist/chat/slack/app/guards.js +162 -0
- package/dist/chat/slack/app/home-controls.js +91 -0
- package/dist/chat/slack/app/messages.js +181 -0
- package/dist/chat/slack/app/open.js +392 -0
- package/dist/chat/slack/app/order.js +37 -0
- package/dist/chat/slack/app/requests.js +482 -0
- package/dist/chat/slack/app/resume-bind.js +71 -0
- package/dist/chat/slack/app/wire.js +83 -0
- package/dist/chat/slack/attachments.js +348 -0
- package/dist/chat/slack/bind.js +67 -0
- package/dist/chat/slack/channel.js +67 -0
- package/dist/chat/slack/channels.js +43 -0
- package/dist/chat/slack/delete.js +278 -0
- package/dist/chat/slack/footer.js +43 -0
- package/dist/chat/slack/hold.js +37 -0
- package/dist/chat/slack/home.js +1069 -0
- package/dist/chat/slack/module.js +174 -0
- package/dist/chat/slack/notice.js +64 -0
- package/dist/chat/slack/openfile/modal.js +362 -0
- package/dist/chat/slack/openfile/read.js +29 -0
- package/dist/chat/slack/quiet-logger.js +52 -0
- package/dist/chat/slack/repair.js +123 -0
- package/dist/chat/slack/reply/blocks.js +372 -0
- package/dist/chat/slack/reply/chars.js +59 -0
- package/dist/chat/slack/reply/clients.js +171 -0
- package/dist/chat/slack/reply/errors.js +125 -0
- package/dist/chat/slack/reply/escape.js +25 -0
- package/dist/chat/slack/reply/limiter.js +86 -0
- package/dist/chat/slack/reply/markdown.js +210 -0
- package/dist/chat/slack/reply/places.js +262 -0
- package/dist/chat/slack/reply/sinks.js +2403 -0
- package/dist/chat/slack/reply/status.js +433 -0
- package/dist/chat/slack/reply/tasks.js +11 -0
- package/dist/chat/slack/requests.js +552 -0
- package/dist/chat/slack/resume.js +127 -0
- package/dist/chat/slack/setup.js +114 -0
- package/dist/chat/slack/texts.js +185 -0
- package/dist/chat/slack/thread.js +537 -0
- package/dist/chat/slack/update.js +71 -0
- package/dist/chat/slack/voice.js +84 -0
- package/dist/cli.js +326 -0
- package/dist/clock.js +21 -0
- package/dist/core/cleanup.js +145 -0
- package/dist/core/commands.js +172 -0
- package/dist/core/config.js +174 -0
- package/dist/core/files/changed.js +125 -0
- package/dist/core/files/listing.js +382 -0
- package/dist/core/files/paths.js +9 -0
- package/dist/core/files/read.js +191 -0
- package/dist/core/files/waiting.js +34 -0
- package/dist/core/flows/answers.js +133 -0
- package/dist/core/flows/ask-owner.js +95 -0
- package/dist/core/flows/asking.js +14 -0
- package/dist/core/flows/bind.js +95 -0
- package/dist/core/flows/contract.js +1 -0
- package/dist/core/flows/flows.js +35 -0
- package/dist/core/flows/hold.js +22 -0
- package/dist/core/flows/inbound.js +24 -0
- package/dist/core/flows/lists.js +21 -0
- package/dist/core/flows/messages.js +293 -0
- package/dist/core/flows/notices.js +51 -0
- package/dist/core/flows/repair.js +106 -0
- package/dist/core/flows/resume.js +163 -0
- package/dist/core/flows/setup.js +130 -0
- package/dist/core/flows/status.js +98 -0
- package/dist/core/flows/talk.js +207 -0
- package/dist/core/flows/update.js +370 -0
- package/dist/core/flows/words.js +171 -0
- package/dist/core/folders.js +93 -0
- package/dist/core/footer.js +313 -0
- package/dist/core/guide.js +50 -0
- package/dist/core/hold.js +126 -0
- package/dist/core/instance.js +46 -0
- package/dist/core/lock.js +412 -0
- package/dist/core/models.js +10 -0
- package/dist/core/paths.js +75 -0
- package/dist/core/port/file-flows.js +1 -0
- package/dist/core/port/home.js +1 -0
- package/dist/core/port/requests.js +1 -0
- package/dist/core/port/threads.js +1 -0
- package/dist/core/port/updates.js +1 -0
- package/dist/core/port.js +17 -0
- package/dist/core/reply/fold.js +175 -0
- package/dist/core/reply/previews.js +258 -0
- package/dist/core/reply/renderer.js +846 -0
- package/dist/core/reply/tail.js +24 -0
- package/dist/core/reply/words.js +32 -0
- package/dist/core/requests.js +158 -0
- package/dist/core/sessions/constants.js +61 -0
- package/dist/core/sessions/deps.js +1 -0
- package/dist/core/sessions/directory.js +86 -0
- package/dist/core/sessions/errors.js +79 -0
- package/dist/core/sessions/manager.js +378 -0
- package/dist/core/sessions/session.js +3297 -0
- package/dist/core/sessions/tasks.js +266 -0
- package/dist/core/sessions/turn.js +45 -0
- package/dist/core/setup.js +28 -0
- package/dist/core/state.js +724 -0
- package/dist/core/texts.js +451 -0
- package/dist/core/updates.js +332 -0
- package/dist/daemon.js +566 -0
- package/dist/host/npm.js +134 -0
- package/dist/host/program.js +49 -0
- package/dist/host/run.js +52 -0
- package/dist/host/service/launchd.js +385 -0
- package/dist/host/service/listing.js +42 -0
- package/dist/host/service/seam.js +1 -0
- package/dist/log.js +37 -0
- package/dist/main.js +29 -0
- package/dist/node-version.js +40 -0
- package/dist/registry.js +12 -0
- package/dist/semver.js +53 -0
- package/dist/text.js +89 -0
- package/dist/updates-host.js +113 -0
- package/dist/values.js +30 -0
- package/dist/version.js +54 -0
- package/package.json +32 -4
- package/slack-app-manifest.json +50 -0
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { failsCounted, folded } from "./previews.js";
|
|
2
|
+
import { STOPPED } from "./words.js";
|
|
3
|
+
export const ENDED = ["complete", "error"];
|
|
4
|
+
export const SEPARATOR = ", ";
|
|
5
|
+
// The most command lines a run's card lists: its last ones, as a subagent's card lists its calls.
|
|
6
|
+
const RUN_COMMANDS = 10;
|
|
7
|
+
/**
|
|
8
|
+
* The preview, on a call that finished well and only there: a failed, running or task card keeps
|
|
9
|
+
* its own view whatever it carries, so no path can show a preview over an error.
|
|
10
|
+
*/
|
|
11
|
+
export function shownPreview(update) {
|
|
12
|
+
return update.status === "complete" && !update.task ? update.preview : null;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Whether a call is drawn on its run's card rather than on a card of its own. A call that failed
|
|
16
|
+
* is counted with the others only where the terminal was seen to count it so (a command that
|
|
17
|
+
* exits with an error, `previews.failsCounted`); any other failed call keeps a card that says
|
|
18
|
+
* why.
|
|
19
|
+
*/
|
|
20
|
+
export function folds(update) {
|
|
21
|
+
if (update.status === "error" && !failsCounted(update.name))
|
|
22
|
+
return false;
|
|
23
|
+
return (update.name !== "" && !update.task && shownPreview(update) === null && update.output !== STOPPED);
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Ended calls as counts in the terminal's words, joined as it joins them: a comma between two
|
|
27
|
+
* counts, and a capital on the first alone. A failed call is counted with the others.
|
|
28
|
+
*/
|
|
29
|
+
export function counts(calls) {
|
|
30
|
+
// In the order each name first ended, as the terminal lists them.
|
|
31
|
+
const byName = new Map();
|
|
32
|
+
for (const call of calls)
|
|
33
|
+
byName.set(call.name, (byName.get(call.name) ?? 0) + 1);
|
|
34
|
+
return [...byName].map(([name, n], index) => folded(name, n, index === 0)).join(SEPARATOR);
|
|
35
|
+
}
|
|
36
|
+
function newRun() {
|
|
37
|
+
return { calls: new Map(), ended: [], card: null, open: true };
|
|
38
|
+
}
|
|
39
|
+
function samePreview(a, b) {
|
|
40
|
+
if (a === null || b === null)
|
|
41
|
+
return a === b;
|
|
42
|
+
return (a.title === b.title &&
|
|
43
|
+
a.summary === b.summary &&
|
|
44
|
+
a.body === b.body &&
|
|
45
|
+
a.language === b.language &&
|
|
46
|
+
a.plain === b.plain &&
|
|
47
|
+
a.record === b.record);
|
|
48
|
+
}
|
|
49
|
+
/** Whether two states of a line say the same, field by field. */
|
|
50
|
+
function sameUpdate(a, b) {
|
|
51
|
+
return (a !== undefined &&
|
|
52
|
+
a.id === b.id &&
|
|
53
|
+
a.title === b.title &&
|
|
54
|
+
a.status === b.status &&
|
|
55
|
+
a.details === b.details &&
|
|
56
|
+
a.output === b.output &&
|
|
57
|
+
a.name === b.name &&
|
|
58
|
+
a.task === b.task &&
|
|
59
|
+
a.calls === b.calls &&
|
|
60
|
+
a.folded === b.folded &&
|
|
61
|
+
a.startedAt === b.startedAt &&
|
|
62
|
+
samePreview(a.preview, b.preview));
|
|
63
|
+
}
|
|
64
|
+
function remove(list, item) {
|
|
65
|
+
const index = list.indexOf(item);
|
|
66
|
+
if (index !== -1)
|
|
67
|
+
list.splice(index, 1);
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Turns the calls of one reply into the cards that show them. `task` returns the cards a call's
|
|
71
|
+
* new state changes, in the order they are to appear; `text` ends the run.
|
|
72
|
+
*/
|
|
73
|
+
export class Fold {
|
|
74
|
+
/** Call id to its run. */
|
|
75
|
+
runs = new Map();
|
|
76
|
+
/** Call id to the card that is its own. */
|
|
77
|
+
own = new Map();
|
|
78
|
+
current = null;
|
|
79
|
+
sent = new Map();
|
|
80
|
+
/** Text was written: the calls after it are a new run. */
|
|
81
|
+
text() {
|
|
82
|
+
if (this.current !== null)
|
|
83
|
+
this.current.open = false;
|
|
84
|
+
}
|
|
85
|
+
task(update) {
|
|
86
|
+
const own = this.own.get(update.id);
|
|
87
|
+
if (own !== undefined)
|
|
88
|
+
return this.changed([{ ...update, id: own }]);
|
|
89
|
+
let run = this.runs.get(update.id);
|
|
90
|
+
if (run === undefined) {
|
|
91
|
+
if (!folds(update)) {
|
|
92
|
+
this.text();
|
|
93
|
+
this.own.set(update.id, update.id);
|
|
94
|
+
return this.changed([update]);
|
|
95
|
+
}
|
|
96
|
+
if (this.current === null || !this.current.open)
|
|
97
|
+
this.current = newRun();
|
|
98
|
+
run = this.current;
|
|
99
|
+
this.runs.set(update.id, run);
|
|
100
|
+
}
|
|
101
|
+
else if (!folds(update)) {
|
|
102
|
+
return this.changed([this.leave(run, update), ...this.draw(run)]);
|
|
103
|
+
}
|
|
104
|
+
run.calls.set(update.id, update);
|
|
105
|
+
if (!ENDED.includes(update.status)) {
|
|
106
|
+
remove(run.ended, update.id);
|
|
107
|
+
}
|
|
108
|
+
else if (!run.ended.includes(update.id)) {
|
|
109
|
+
run.ended.push(update.id);
|
|
110
|
+
}
|
|
111
|
+
return this.changed(this.draw(run));
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* A call that no longer folds gets a card of its own. The run's only call keeps the run's
|
|
115
|
+
* card, which showed it. The run takes no new call.
|
|
116
|
+
*/
|
|
117
|
+
leave(run, update) {
|
|
118
|
+
run.calls.delete(update.id);
|
|
119
|
+
this.runs.delete(update.id);
|
|
120
|
+
remove(run.ended, update.id);
|
|
121
|
+
run.open = false;
|
|
122
|
+
let card = update.id;
|
|
123
|
+
if (run.calls.size === 0 && run.card !== null) {
|
|
124
|
+
card = run.card;
|
|
125
|
+
run.card = null;
|
|
126
|
+
}
|
|
127
|
+
this.own.set(update.id, card);
|
|
128
|
+
return { ...update, id: card };
|
|
129
|
+
}
|
|
130
|
+
/** The run's card as it stands: the call that runs now, else the counts of what ended. */
|
|
131
|
+
draw(run) {
|
|
132
|
+
const first = run.calls.values().next().value;
|
|
133
|
+
if (first === undefined)
|
|
134
|
+
return [];
|
|
135
|
+
// Named after the run's first call: a call that takes a card of its own is named by its id.
|
|
136
|
+
run.card ??= `fold:${first.id}`;
|
|
137
|
+
// The command of each timed call of the run, which the card lists when it is expanded.
|
|
138
|
+
const lines = [...run.calls.values()].flatMap((call) => call.startedAt !== null && call.details !== null ? [call.details] : []);
|
|
139
|
+
const details = lines.length === 0 ? null : lines.slice(-RUN_COMMANDS).join("\n");
|
|
140
|
+
const running = [...run.calls.values()].filter((call) => !ENDED.includes(call.status));
|
|
141
|
+
const now = running.at(-1);
|
|
142
|
+
// A call still running when the reply's body ends has no count yet and stays a card.
|
|
143
|
+
if (now !== undefined) {
|
|
144
|
+
return [card(run.card, now.title, now.status, now.name, null, now.startedAt, details)];
|
|
145
|
+
}
|
|
146
|
+
const ended = run.ended.flatMap((id) => run.calls.get(id) ?? []);
|
|
147
|
+
const line = counts(ended);
|
|
148
|
+
const status = ended.some((call) => call.status === "complete")
|
|
149
|
+
? "complete"
|
|
150
|
+
: "error";
|
|
151
|
+
return [card(run.card, line, status, "", line, null, details)];
|
|
152
|
+
}
|
|
153
|
+
changed(cards) {
|
|
154
|
+
const out = cards.filter((card) => !sameUpdate(this.sent.get(card.id), card));
|
|
155
|
+
for (const card of out)
|
|
156
|
+
this.sent.set(card.id, card);
|
|
157
|
+
return out;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
/** A run's card: a title and a state, and the line that replaces it at the reply's end. */
|
|
161
|
+
function card(id, title, status, name, line, startedAt, details) {
|
|
162
|
+
return {
|
|
163
|
+
id,
|
|
164
|
+
title,
|
|
165
|
+
status,
|
|
166
|
+
details,
|
|
167
|
+
output: null,
|
|
168
|
+
name,
|
|
169
|
+
task: false,
|
|
170
|
+
calls: 0,
|
|
171
|
+
preview: null,
|
|
172
|
+
folded: line,
|
|
173
|
+
startedAt,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import { ANSWERED, fill, named, TASK_LAUNCHED, TASKS_LAUNCHED, TASKS_LAUNCHED_BRANCH, TASKS_LAUNCHED_LAST, } from "../texts.js";
|
|
2
|
+
import { oneLine, spaced, splitLines, TITLE_LIMIT } from "./words.js";
|
|
3
|
+
// How the terminal folds finished calls of these tools. Captured from the terminal on Claude Code
|
|
4
|
+
// 2.1.283 (2026-09-27), where Bash also does the searching (the CLI has no Grep or Glob tool):
|
|
5
|
+
// `echo hi` read `Ran 1 shell command`, `ls` `Listed 1 directory`, a grep `Searched for 1
|
|
6
|
+
// pattern`. That classification of the command is undocumented, so every Bash call reads here as
|
|
7
|
+
// a shell command.
|
|
8
|
+
export const WORDS = {
|
|
9
|
+
Bash: ["Ran {n} shell command", "Ran {n} shell commands"],
|
|
10
|
+
Read: ["Read {n} file", "Read {n} files"],
|
|
11
|
+
};
|
|
12
|
+
// A diff line's colour where Slack draws none (mobile); an emoji is two columns wide, so a
|
|
13
|
+
// context line gets two spaces and the numbers stay aligned.
|
|
14
|
+
export const MARKS = {
|
|
15
|
+
"-": "\u{1f7e5}",
|
|
16
|
+
"+": "\u{1f7e9}",
|
|
17
|
+
" ": " ",
|
|
18
|
+
};
|
|
19
|
+
// The terminal shows a new file's first lines, then how many it leaves out.
|
|
20
|
+
export const NEW_FILE_LINES = 10;
|
|
21
|
+
// The tools `preview` reads. A reply shows a call of one only once it has ended: a stream cannot
|
|
22
|
+
// take back a card, and a call that ended well is its preview alone.
|
|
23
|
+
export const PREVIEWED = new Set(["Edit", "Write"]);
|
|
24
|
+
// The tools whose call a reply holds until it ends, as for `PREVIEWED`: a question the owner
|
|
25
|
+
// answers leaves a record in place of its card (`answered`).
|
|
26
|
+
export const ASKERS = new Set(["AskUserQuestion"]);
|
|
27
|
+
// The tools that start an agent. A reply holds a call of one until its task says how it runs: in
|
|
28
|
+
// the background the terminal writes a line and no more (`launched`), and a stream cannot take
|
|
29
|
+
// back a card.
|
|
30
|
+
export const LAUNCHERS = new Set(["Agent"]);
|
|
31
|
+
// The type the terminal does not name in an agent's launch line: it reads `Agent(...)`, the
|
|
32
|
+
// call's name, as for no type.
|
|
33
|
+
const PLAIN_AGENT = "general-purpose";
|
|
34
|
+
/**
|
|
35
|
+
* The launch line of a task started in the background: the agent's type when it names one, else
|
|
36
|
+
* the name of the call that started it, then what the task does.
|
|
37
|
+
*/
|
|
38
|
+
export function launched(launch) {
|
|
39
|
+
const { name, agentType, description, model } = launch;
|
|
40
|
+
const shown = agentType === null || agentType === PLAIN_AGENT ? name : agentType;
|
|
41
|
+
return fill(TASK_LAUNCHED, {
|
|
42
|
+
name: shown,
|
|
43
|
+
description: oneLine(description, TITLE_LIMIT),
|
|
44
|
+
model: model === null ? "" : ` ${model}`,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* The lines for the tasks one message started in the background: one task's own line, or for
|
|
49
|
+
* several a count and what each does, with no name.
|
|
50
|
+
*/
|
|
51
|
+
export function launchedTogether(launches) {
|
|
52
|
+
const [only] = launches;
|
|
53
|
+
if (only !== undefined && launches.length === 1)
|
|
54
|
+
return launched(only);
|
|
55
|
+
const lines = launches.map(({ description }, index) => fill(index === launches.length - 1 ? TASKS_LAUNCHED_LAST : TASKS_LAUNCHED_BRANCH, {
|
|
56
|
+
description: oneLine(description, TITLE_LIMIT),
|
|
57
|
+
}));
|
|
58
|
+
return [fill(TASKS_LAUNCHED, { n: launches.length }), ...lines].join("\n");
|
|
59
|
+
}
|
|
60
|
+
// The tools whose failed call the terminal counts with the others and says nothing more of: a
|
|
61
|
+
// command that exits with an error was seen counted in `ran 4 shell commands` (captured on
|
|
62
|
+
// Claude Code 2.1.296, 2026-10-13). No other tool's failure was captured.
|
|
63
|
+
const FAILS_COUNTED = new Set(["Bash"]);
|
|
64
|
+
/** Whether a failed call of that tool is counted in its run with the calls that ended well. */
|
|
65
|
+
export function failsCounted(name) {
|
|
66
|
+
return FAILS_COUNTED.has(name);
|
|
67
|
+
}
|
|
68
|
+
// The input the terminal titles a running call by: a command's line reads its description
|
|
69
|
+
// (`Sleeping 20 seconds then printing marker`), with the command itself under it.
|
|
70
|
+
const DESCRIBED = { Bash: "description" };
|
|
71
|
+
/** The terminal's title for a call that runs, where it has one of its own; null for any other. */
|
|
72
|
+
export function described(name, input) {
|
|
73
|
+
const said = Object.hasOwn(DESCRIBED, name) ? input[DESCRIBED[name]] : undefined;
|
|
74
|
+
return typeof said === "string" && said.trim() !== "" ? oneLine(said, TITLE_LIMIT) : null;
|
|
75
|
+
}
|
|
76
|
+
// The input a timed call shows under its title, after a prompt mark: a running command's line in
|
|
77
|
+
// the terminal reads `$ <command> (5s)` (captured on Claude Code 2.1.301, 2026-10-16).
|
|
78
|
+
const COMMANDED = { Bash: "command" };
|
|
79
|
+
const COMMAND_LIMIT = 200;
|
|
80
|
+
/**
|
|
81
|
+
* The line a call the terminal times lists under its title (`$ sleep 20`); null for any other
|
|
82
|
+
* call, and for one whose input holds no text there.
|
|
83
|
+
*/
|
|
84
|
+
export function commandLine(name, input) {
|
|
85
|
+
const said = Object.hasOwn(COMMANDED, name) ? input[COMMANDED[name]] : undefined;
|
|
86
|
+
return typeof said === "string" && said.trim() !== ""
|
|
87
|
+
? `$ ${oneLine(said, COMMAND_LIMIT)}`
|
|
88
|
+
: null;
|
|
89
|
+
}
|
|
90
|
+
// The calls that continue an agent already started, and the input that names it: the terminal
|
|
91
|
+
// draws no line for such a call, and writes `Resuming agent` under the agent (captured on
|
|
92
|
+
// Claude Code 2.1.301, 2026-10-16).
|
|
93
|
+
const CONTINUERS = { SendMessage: "to" };
|
|
94
|
+
const RESUMING = "\u23bf Resuming agent {id}";
|
|
95
|
+
// The terminal names an agent by the first characters of its id.
|
|
96
|
+
const AGENT_ID_CHARS = 7;
|
|
97
|
+
/** The task a call continues, when its tool is one that continues an agent; null otherwise. */
|
|
98
|
+
export function continued(name, input) {
|
|
99
|
+
const to = Object.hasOwn(CONTINUERS, name) ? input[CONTINUERS[name]] : undefined;
|
|
100
|
+
return typeof to === "string" && to !== "" ? to : null;
|
|
101
|
+
}
|
|
102
|
+
/** The line that says the task `taskId` is resumed. */
|
|
103
|
+
export function resuming(taskId) {
|
|
104
|
+
return fill(RESUMING, { id: taskId.slice(0, AGENT_ID_CHARS) });
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* How `n` finished calls of `name` read in a folded line. The terminal's words take a capital
|
|
108
|
+
* only when they open the line (`Read 1 file, ran 4 shell commands`); a tool's name is its own.
|
|
109
|
+
*/
|
|
110
|
+
export function folded(name, n, first = true) {
|
|
111
|
+
const words = Object.hasOwn(WORDS, name) ? WORDS[name] : undefined;
|
|
112
|
+
if (words === undefined) {
|
|
113
|
+
return n === 1 ? name : `${name} ×${n}`;
|
|
114
|
+
}
|
|
115
|
+
const said = fill(n === 1 ? words[0] : words[1], { n });
|
|
116
|
+
return first ? said : said.charAt(0).toLowerCase() + said.slice(1);
|
|
117
|
+
}
|
|
118
|
+
function counted(n, noun = "line") {
|
|
119
|
+
return `${n} ${noun}${n === 1 ? "" : "s"}`;
|
|
120
|
+
}
|
|
121
|
+
/** The parts of a POSIX path as Python's `PurePosixPath` keeps them: no empty part, no `.`. */
|
|
122
|
+
function parts(path) {
|
|
123
|
+
return path.split("/").filter((part) => part !== "" && part !== ".");
|
|
124
|
+
}
|
|
125
|
+
/** The path as the terminal names it: relative to the session's folder when inside it. */
|
|
126
|
+
function shown(path, cwd) {
|
|
127
|
+
if (!cwd)
|
|
128
|
+
return path;
|
|
129
|
+
const base = parts(cwd);
|
|
130
|
+
const inner = parts(path);
|
|
131
|
+
const inside = path.startsWith("/") === cwd.startsWith("/") &&
|
|
132
|
+
base.length <= inner.length &&
|
|
133
|
+
base.every((part, index) => inner[index] === part);
|
|
134
|
+
if (!inside)
|
|
135
|
+
return path;
|
|
136
|
+
return inner.slice(base.length).join("/") || ".";
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Lines added and removed, and the hunks numbered as the terminal numbers them: a removed line
|
|
140
|
+
* by its old number, any other by its new one, `...` between hunks. The sign leads the line,
|
|
141
|
+
* where Slack desktop's diff highlighting looks for it; a coloured square follows, since Slack
|
|
142
|
+
* mobile does not highlight (both measured 2026-09-27): `-🟥 7 sette`, `+🟩 7 7`.
|
|
143
|
+
*/
|
|
144
|
+
function diff(hunks) {
|
|
145
|
+
let added = 0;
|
|
146
|
+
let removed = 0;
|
|
147
|
+
const rows = [];
|
|
148
|
+
for (const [index, hunk] of hunks.entries()) {
|
|
149
|
+
let { oldStart: old, newStart: fresh } = hunk;
|
|
150
|
+
if (index > 0)
|
|
151
|
+
rows.push([0, "..."]);
|
|
152
|
+
for (const line of hunk.lines) {
|
|
153
|
+
const sign = line.charAt(0);
|
|
154
|
+
const text = line.slice(1);
|
|
155
|
+
if (sign === "-") {
|
|
156
|
+
rows.push([old, `-${text}`]);
|
|
157
|
+
old += 1;
|
|
158
|
+
removed += 1;
|
|
159
|
+
}
|
|
160
|
+
else if (sign === "+") {
|
|
161
|
+
rows.push([fresh, `+${text}`]);
|
|
162
|
+
fresh += 1;
|
|
163
|
+
added += 1;
|
|
164
|
+
}
|
|
165
|
+
else if (sign === " ") {
|
|
166
|
+
rows.push([fresh, ` ${text}`]);
|
|
167
|
+
old += 1;
|
|
168
|
+
fresh += 1;
|
|
169
|
+
}
|
|
170
|
+
else if (sign !== "\\") {
|
|
171
|
+
// `` is about the line above: skipped
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
const width = String(Math.max(0, ...rows.map(([n]) => n))).length;
|
|
177
|
+
const body = rows.map(([n, text]) => {
|
|
178
|
+
const sign = text.charAt(0);
|
|
179
|
+
return n === 0 ? text : `${sign}${MARKS[sign]} ${String(n).padStart(width)} ${text.slice(1)}`;
|
|
180
|
+
});
|
|
181
|
+
return { added, removed, body };
|
|
182
|
+
}
|
|
183
|
+
function changed(added, removed) {
|
|
184
|
+
const said = added > 0 ? [`Added ${counted(added)}`] : [];
|
|
185
|
+
if (removed > 0) {
|
|
186
|
+
said.push(said.length > 0 ? `removed ${counted(removed)}` : `Removed ${counted(removed)}`);
|
|
187
|
+
}
|
|
188
|
+
return said.join(", ") || "No change";
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* The answers to a question as the terminal keeps them: a line per question that has one,
|
|
192
|
+
* `· question → answer`, a multi-select's answers joined by commas. Null with no answer.
|
|
193
|
+
* `names` fill the title, which names who wrote the questions.
|
|
194
|
+
*/
|
|
195
|
+
export function answered(questions, answers, names) {
|
|
196
|
+
const lines = [];
|
|
197
|
+
for (const question of questions) {
|
|
198
|
+
const answer = Object.hasOwn(answers, question.text) ? (answers[question.text] ?? "") : "";
|
|
199
|
+
const said = typeof answer === "string" ? answer : answer.join(", ");
|
|
200
|
+
if (said !== "") {
|
|
201
|
+
// One line each, however the question or a typed answer was written.
|
|
202
|
+
lines.push(`· ${spaced(question.text)} → ${spaced(said)}`);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
if (lines.length === 0)
|
|
206
|
+
return null;
|
|
207
|
+
return {
|
|
208
|
+
title: named(ANSWERED, names),
|
|
209
|
+
summary: "",
|
|
210
|
+
body: lines.join("\n"),
|
|
211
|
+
language: "",
|
|
212
|
+
plain: true,
|
|
213
|
+
record: true,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* The terminal's view of a finished `Edit` or `Write` from what it did to a file, or null for
|
|
218
|
+
* any other tool and for a call that reports no change. `cwd` is the session's folder.
|
|
219
|
+
*/
|
|
220
|
+
export function preview(name, change, cwd) {
|
|
221
|
+
if (change === null)
|
|
222
|
+
return null;
|
|
223
|
+
const path = shown(change.path, cwd);
|
|
224
|
+
if (name === "Write" && change.kind === "created") {
|
|
225
|
+
if (change.content === null)
|
|
226
|
+
return null;
|
|
227
|
+
const lines = splitLines(change.content);
|
|
228
|
+
const width = String(Math.min(lines.length, NEW_FILE_LINES)).length;
|
|
229
|
+
const body = lines
|
|
230
|
+
.slice(0, NEW_FILE_LINES)
|
|
231
|
+
.map((line, index) => `${String(index + 1).padStart(width)} ${line}`);
|
|
232
|
+
if (lines.length > NEW_FILE_LINES)
|
|
233
|
+
body.push(`… +${counted(lines.length - NEW_FILE_LINES)}`);
|
|
234
|
+
return {
|
|
235
|
+
title: `Write(${path})`,
|
|
236
|
+
summary: `Wrote ${counted(lines.length)} to ${path}`,
|
|
237
|
+
body: body.join("\n"),
|
|
238
|
+
language: "",
|
|
239
|
+
plain: false,
|
|
240
|
+
record: false,
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
if (PREVIEWED.has(name) && change.hunks.length > 0) {
|
|
244
|
+
const numbered = diff(change.hunks);
|
|
245
|
+
if (numbered === null)
|
|
246
|
+
return null;
|
|
247
|
+
// The terminal names an edit `Update`, and a Write over an existing file keeps `Write`.
|
|
248
|
+
return {
|
|
249
|
+
title: `${name === "Edit" ? "Update" : "Write"}(${path})`,
|
|
250
|
+
summary: changed(numbered.added, numbered.removed),
|
|
251
|
+
body: numbered.body.join("\n"),
|
|
252
|
+
language: "diff",
|
|
253
|
+
plain: false,
|
|
254
|
+
record: false,
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
return null;
|
|
258
|
+
}
|