@oharness/server 0.10.0 → 0.12.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/dist/analytics.d.ts +32 -29
- package/dist/analytics.js +225 -278
- package/dist/analytics.js.map +1 -1
- package/dist/i18n.js +242 -12
- package/dist/i18n.js.map +1 -1
- package/dist/icons.d.ts +3 -0
- package/dist/icons.js +69 -3
- package/dist/icons.js.map +1 -1
- package/dist/model-order.js +1 -0
- package/dist/model-order.js.map +1 -1
- package/dist/page/client/account.js +140 -4
- package/dist/page/client/account.js.map +1 -1
- package/dist/page/client/base.js +42 -16
- package/dist/page/client/base.js.map +1 -1
- package/dist/page/client/browser.js +1 -1
- package/dist/page/client/composer.js +641 -47
- package/dist/page/client/composer.js.map +1 -1
- package/dist/page/client/dialogs.js +75 -0
- package/dist/page/client/dialogs.js.map +1 -1
- package/dist/page/client/events.js +25 -2
- package/dist/page/client/events.js.map +1 -1
- package/dist/page/client/index.js +4 -0
- package/dist/page/client/index.js.map +1 -1
- package/dist/page/client/panels.js +4 -4
- package/dist/page/client/ripple.d.ts +13 -0
- package/dist/page/client/ripple.js +247 -0
- package/dist/page/client/ripple.js.map +1 -0
- package/dist/page/client/routines.js +173 -41
- package/dist/page/client/routines.js.map +1 -1
- package/dist/page/client/shell.js +200 -54
- package/dist/page/client/shell.js.map +1 -1
- package/dist/page/client/state.js +7 -2
- package/dist/page/client/state.js.map +1 -1
- package/dist/page/client/terminal.js +34 -7
- package/dist/page/client/terminal.js.map +1 -1
- package/dist/page/client/theme.d.ts +17 -0
- package/dist/page/client/theme.js +365 -0
- package/dist/page/client/theme.js.map +1 -0
- package/dist/page/client/transcript.js +63 -7
- package/dist/page/client/transcript.js.map +1 -1
- package/dist/page/style.js +273 -22
- package/dist/page/style.js.map +1 -1
- package/dist/page.js +5 -5
- package/dist/page.js.map +1 -1
- package/dist/prefs.d.ts +17 -0
- package/dist/prefs.js +5 -0
- package/dist/prefs.js.map +1 -1
- package/dist/server.js +710 -45
- package/dist/server.js.map +1 -1
- package/dist/suggest.d.ts +42 -0
- package/dist/suggest.js +215 -0
- package/dist/suggest.js.map +1 -0
- package/dist/terminal.d.ts +35 -2
- package/dist/terminal.js +60 -4
- package/dist/terminal.js.map +1 -1
- package/dist/theme-assets.d.ts +39 -0
- package/dist/theme-assets.js +109 -0
- package/dist/theme-assets.js.map +1 -0
- package/dist/theme-store.d.ts +50 -0
- package/dist/theme-store.js +124 -0
- package/dist/theme-store.js.map +1 -0
- package/dist/theme-tools.d.ts +25 -0
- package/dist/theme-tools.js +243 -0
- package/dist/theme-tools.js.map +1 -0
- package/dist/tool-icons.d.ts +12 -0
- package/dist/tool-icons.js +16 -0
- package/dist/tool-icons.js.map +1 -0
- package/dist/turn-shots.d.ts +18 -2
- package/dist/turn-shots.js +10 -1
- package/dist/turn-shots.js.map +1 -1
- package/dist/user-theme.d.ts +143 -0
- package/dist/user-theme.js +510 -0
- package/dist/user-theme.js.map +1 -0
- package/package.json +2 -2
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { Message } from "@oharness/engine/core/types.ts";
|
|
2
|
+
import type { Harness } from "@oharness/engine/harness.ts";
|
|
3
|
+
/**
|
|
4
|
+
* The grey line in the composer: what the user is likely to type next.
|
|
5
|
+
*
|
|
6
|
+
* Written once a turn has ended on its own, from the last few exchanges — the
|
|
7
|
+
* requests, the replies' final answers and, for the latest, a line per tool
|
|
8
|
+
* call; never tool output — and offered for Tab to
|
|
9
|
+
* take. On the conversation's own model, the way Claude Code does it: the
|
|
10
|
+
* guess reads like the agent the user is talking to, and costs what that
|
|
11
|
+
* model costs, which the Settings switch is there to turn off.
|
|
12
|
+
*
|
|
13
|
+
* Absent whenever the model is unsure: an empty box is better than a wrong
|
|
14
|
+
* prediction sitting in it.
|
|
15
|
+
*/
|
|
16
|
+
export declare const SUGGEST_PROMPT = "You predict the next message a user will send to their AI agent, so it can be offered in their input box for them to accept with one key.\n\nYou are given the last few exchanges of the conversation, oldest first. For the latest one you also see, in <agent_actions>, the tools the agent used on the way to its answer: what it read, changed and ran, and what failed.\n\nWrite the single most likely next message, as the user would type it:\n- a short instruction, at most 12 words, in the same language the user wrote in;\n- grounded in the latest reply: approving what the agent proposed, answering a question it asked, or picking up something it said was left unfinished or failing;\n- specific to this conversation, never generic (\"thanks\", \"continue\", \"ok\");\n- never something the agent already did, and never a new task the conversation does not point to.\n\nIf the user has wrapped up (thanked the agent, said it is done) or the latest reply leaves nothing open, reply with exactly NONE.\nReply with only the message or NONE: no quotes, no preamble.";
|
|
17
|
+
/** One request, the answer it got, and what the agent did in between. */
|
|
18
|
+
export interface Exchange {
|
|
19
|
+
ask: string;
|
|
20
|
+
reply: string;
|
|
21
|
+
actions: string[];
|
|
22
|
+
}
|
|
23
|
+
/** Roughly what a tokenizer makes of it: a token per CJK character, four of anything else. */
|
|
24
|
+
export declare function estimateTokens(text: string): number;
|
|
25
|
+
/** Cut to `tokens`, from the start, the end, or both ends with the middle elided. */
|
|
26
|
+
export declare function cut(text: string, tokens: number, keep: "head" | "tail" | "ends"): string;
|
|
27
|
+
/**
|
|
28
|
+
* The last few exchanges, oldest first, cut to their budgets and redacted.
|
|
29
|
+
*
|
|
30
|
+
* A request is a user message with text that the harness did not write:
|
|
31
|
+
* tool results come back as user messages with none, and a compaction
|
|
32
|
+
* summary, a task notification or a goal nudge are marked synthetic. The
|
|
33
|
+
* reply is the agent's final answer — the text after its last tool call —
|
|
34
|
+
* because the narration on the way ("let me look at X") is not what the user
|
|
35
|
+
* answers; when a turn ends on a call, its last words are used instead.
|
|
36
|
+
*/
|
|
37
|
+
export declare function recentExchanges(messages: readonly Message[], count?: number): Exchange[];
|
|
38
|
+
/** The exchanges as the model reads them. */
|
|
39
|
+
export declare function suggestContext(messages: readonly Message[]): string | undefined;
|
|
40
|
+
/** One line fit for the box, or nothing: a long, multi-line or quoted answer is the model not following the brief. */
|
|
41
|
+
export declare function cleanSuggestion(raw: string): string | undefined;
|
|
42
|
+
export declare function suggestNext(harness: Harness, messages: readonly Message[], model: string, signal: AbortSignal): Promise<string | undefined>;
|
package/dist/suggest.js
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import { visibleText } from "@oharness/engine/core/types.ts";
|
|
2
|
+
import { redactSecrets } from "@oharness/engine/agent/memory-redact.ts";
|
|
3
|
+
/**
|
|
4
|
+
* The grey line in the composer: what the user is likely to type next.
|
|
5
|
+
*
|
|
6
|
+
* Written once a turn has ended on its own, from the last few exchanges — the
|
|
7
|
+
* requests, the replies' final answers and, for the latest, a line per tool
|
|
8
|
+
* call; never tool output — and offered for Tab to
|
|
9
|
+
* take. On the conversation's own model, the way Claude Code does it: the
|
|
10
|
+
* guess reads like the agent the user is talking to, and costs what that
|
|
11
|
+
* model costs, which the Settings switch is there to turn off.
|
|
12
|
+
*
|
|
13
|
+
* Absent whenever the model is unsure: an empty box is better than a wrong
|
|
14
|
+
* prediction sitting in it.
|
|
15
|
+
*/
|
|
16
|
+
export const SUGGEST_PROMPT = `You predict the next message a user will send to their AI agent, so it can be offered in their input box for them to accept with one key.
|
|
17
|
+
|
|
18
|
+
You are given the last few exchanges of the conversation, oldest first. For the latest one you also see, in <agent_actions>, the tools the agent used on the way to its answer: what it read, changed and ran, and what failed.
|
|
19
|
+
|
|
20
|
+
Write the single most likely next message, as the user would type it:
|
|
21
|
+
- a short instruction, at most 12 words, in the same language the user wrote in;
|
|
22
|
+
- grounded in the latest reply: approving what the agent proposed, answering a question it asked, or picking up something it said was left unfinished or failing;
|
|
23
|
+
- specific to this conversation, never generic ("thanks", "continue", "ok");
|
|
24
|
+
- never something the agent already did, and never a new task the conversation does not point to.
|
|
25
|
+
|
|
26
|
+
If the user has wrapped up (thanked the agent, said it is done) or the latest reply leaves nothing open, reply with exactly NONE.
|
|
27
|
+
Reply with only the message or NONE: no quotes, no preamble.`;
|
|
28
|
+
/**
|
|
29
|
+
* What the guess reads, in estimated tokens rather than characters: a Chinese
|
|
30
|
+
* character is about a token and an English one about a quarter, so a budget
|
|
31
|
+
* in characters gave a Chinese conversation three times the context, and the
|
|
32
|
+
* bill to match.
|
|
33
|
+
*
|
|
34
|
+
* Most of it goes to the exchange that just ended — its request kept at both
|
|
35
|
+
* ends, where a pasted log's actual question sits, and its reply's final
|
|
36
|
+
* answer — and a little to each one before it, enough to say what the thread
|
|
37
|
+
* is about. With these caps three exchanges always fit.
|
|
38
|
+
*/
|
|
39
|
+
const EXCHANGES = 3;
|
|
40
|
+
const LATEST_ASK_TOKENS = 400;
|
|
41
|
+
const LATEST_REPLY_TOKENS = 450;
|
|
42
|
+
const EARLIER_ASK_TOKENS = 80;
|
|
43
|
+
const EARLIER_REPLY_TOKENS = 80;
|
|
44
|
+
/** The actions line: the latest turn's tool calls, most recent kept. */
|
|
45
|
+
const MAX_ACTIONS = 12;
|
|
46
|
+
const ARG_TOKENS = 20;
|
|
47
|
+
const MAX_LENGTH = 120;
|
|
48
|
+
const TIMEOUT_MS = 15_000;
|
|
49
|
+
const WIDE = /[\u1100-\u11ff\u2e80-\ua4cf\uac00-\ud7af\uf900-\ufaff\ufe30-\ufe4f\uff00-\uffef]/;
|
|
50
|
+
/** Roughly what a tokenizer makes of it: a token per CJK character, four of anything else. */
|
|
51
|
+
export function estimateTokens(text) {
|
|
52
|
+
let tokens = 0;
|
|
53
|
+
for (const char of text)
|
|
54
|
+
tokens += WIDE.test(char) ? 1 : 0.25;
|
|
55
|
+
return Math.ceil(tokens);
|
|
56
|
+
}
|
|
57
|
+
/** The longest start, or end, of `text` within `tokens`. */
|
|
58
|
+
function fit(text, tokens, from) {
|
|
59
|
+
const chars = [...text];
|
|
60
|
+
let used = 0;
|
|
61
|
+
let n = 0;
|
|
62
|
+
for (; n < chars.length; n++) {
|
|
63
|
+
const char = chars[from === "head" ? n : chars.length - 1 - n];
|
|
64
|
+
used += WIDE.test(char) ? 1 : 0.25;
|
|
65
|
+
if (used > tokens)
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
return from === "head" ? chars.slice(0, n).join("") : chars.slice(chars.length - n).join("");
|
|
69
|
+
}
|
|
70
|
+
/** Cut to `tokens`, from the start, the end, or both ends with the middle elided. */
|
|
71
|
+
export function cut(text, tokens, keep) {
|
|
72
|
+
if (estimateTokens(text) <= tokens)
|
|
73
|
+
return text;
|
|
74
|
+
if (keep === "head")
|
|
75
|
+
return fit(text, tokens, "head") + "…";
|
|
76
|
+
if (keep === "tail")
|
|
77
|
+
return "…" + fit(text, tokens, "tail");
|
|
78
|
+
return fit(text, tokens / 2, "head") + "\n…\n" + fit(text, tokens / 2, "tail");
|
|
79
|
+
}
|
|
80
|
+
/** The argument that says what a call was about: a path, a command, a pattern. */
|
|
81
|
+
const KEY_ARGS = ["path", "file_path", "command", "pattern", "url", "query", "description", "name"];
|
|
82
|
+
function describeCall(name, input, failed) {
|
|
83
|
+
let arg = "";
|
|
84
|
+
if (input && typeof input === "object") {
|
|
85
|
+
for (const key of KEY_ARGS) {
|
|
86
|
+
const value = input[key];
|
|
87
|
+
if (typeof value === "string" && value.trim()) {
|
|
88
|
+
arg = cut(redactSecrets(value.replace(/\s+/g, " ").trim()).text, ARG_TOKENS, "head");
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return `${name}${arg ? ` ${JSON.stringify(arg)}` : ""}${failed ? " → failed" : ""}`;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* A run of the same call is one line with a count: twenty reads of one file
|
|
97
|
+
* say less than "read ×20", and the room is for what else happened.
|
|
98
|
+
*/
|
|
99
|
+
function collapse(actions) {
|
|
100
|
+
const out = [];
|
|
101
|
+
for (const line of actions) {
|
|
102
|
+
const last = out.at(-1);
|
|
103
|
+
if (last && last.line === line)
|
|
104
|
+
last.times++;
|
|
105
|
+
else
|
|
106
|
+
out.push({ line, times: 1 });
|
|
107
|
+
}
|
|
108
|
+
const lines = out.map(({ line, times }) => (times > 1 ? `${line} ×${times}` : line));
|
|
109
|
+
if (lines.length <= MAX_ACTIONS)
|
|
110
|
+
return lines;
|
|
111
|
+
return [`(${lines.length - MAX_ACTIONS} earlier)`, ...lines.slice(-MAX_ACTIONS)];
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* The last few exchanges, oldest first, cut to their budgets and redacted.
|
|
115
|
+
*
|
|
116
|
+
* A request is a user message with text that the harness did not write:
|
|
117
|
+
* tool results come back as user messages with none, and a compaction
|
|
118
|
+
* summary, a task notification or a goal nudge are marked synthetic. The
|
|
119
|
+
* reply is the agent's final answer — the text after its last tool call —
|
|
120
|
+
* because the narration on the way ("let me look at X") is not what the user
|
|
121
|
+
* answers; when a turn ends on a call, its last words are used instead.
|
|
122
|
+
*/
|
|
123
|
+
export function recentExchanges(messages, count = EXCHANGES) {
|
|
124
|
+
const exchanges = [];
|
|
125
|
+
let after = [];
|
|
126
|
+
for (let i = messages.length - 1; i >= 0 && exchanges.length < count; i--) {
|
|
127
|
+
const message = messages[i];
|
|
128
|
+
const text = message.role === "user" && !message.meta?.synthetic ? visibleText(message).trim() : "";
|
|
129
|
+
if (!text) {
|
|
130
|
+
after.unshift(message);
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
const turn = after;
|
|
134
|
+
after = [];
|
|
135
|
+
const latest = exchanges.length === 0;
|
|
136
|
+
const reply = finalAnswer(turn);
|
|
137
|
+
if (!reply)
|
|
138
|
+
continue;
|
|
139
|
+
exchanges.unshift({
|
|
140
|
+
// Redacted whole, then cut: a key cut in half no longer looks like one.
|
|
141
|
+
ask: cut(redactSecrets(text).text, latest ? LATEST_ASK_TOKENS : EARLIER_ASK_TOKENS, latest ? "ends" : "head"),
|
|
142
|
+
reply: cut(redactSecrets(reply).text, latest ? LATEST_REPLY_TOKENS : EARLIER_REPLY_TOKENS, "tail"),
|
|
143
|
+
actions: latest ? collapse(actionsOf(turn)) : [],
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
return exchanges;
|
|
147
|
+
}
|
|
148
|
+
function finalAnswer(turn) {
|
|
149
|
+
let final = [];
|
|
150
|
+
let last = "";
|
|
151
|
+
for (const message of turn) {
|
|
152
|
+
if (message.role !== "assistant")
|
|
153
|
+
continue;
|
|
154
|
+
for (const block of message.content) {
|
|
155
|
+
if (block.type === "tool_call")
|
|
156
|
+
final = [];
|
|
157
|
+
else if (block.type === "text" && block.text.trim()) {
|
|
158
|
+
final.push(block.text.trim());
|
|
159
|
+
last = block.text.trim();
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return final.length ? final.join("\n") : last;
|
|
164
|
+
}
|
|
165
|
+
function actionsOf(turn) {
|
|
166
|
+
const failed = new Set();
|
|
167
|
+
for (const message of turn) {
|
|
168
|
+
for (const block of message.content) {
|
|
169
|
+
if (block.type === "tool_result" && block.isError)
|
|
170
|
+
failed.add(block.toolCallId);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
const actions = [];
|
|
174
|
+
for (const message of turn) {
|
|
175
|
+
if (message.role !== "assistant")
|
|
176
|
+
continue;
|
|
177
|
+
for (const block of message.content) {
|
|
178
|
+
if (block.type === "tool_call")
|
|
179
|
+
actions.push(describeCall(block.name, block.input, failed.has(block.id)));
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return actions;
|
|
183
|
+
}
|
|
184
|
+
/** The exchanges as the model reads them. */
|
|
185
|
+
export function suggestContext(messages) {
|
|
186
|
+
const exchanges = recentExchanges(messages);
|
|
187
|
+
if (!exchanges.length)
|
|
188
|
+
return undefined;
|
|
189
|
+
return exchanges
|
|
190
|
+
.map((exchange) => {
|
|
191
|
+
const parts = [`<user>\n${exchange.ask}\n</user>`];
|
|
192
|
+
if (exchange.actions.length)
|
|
193
|
+
parts.push(`<agent_actions>\n${exchange.actions.map((line) => `- ${line}`).join("\n")}\n</agent_actions>`);
|
|
194
|
+
parts.push(`<agent>\n${exchange.reply}\n</agent>`);
|
|
195
|
+
return parts.join("\n");
|
|
196
|
+
})
|
|
197
|
+
.join("\n");
|
|
198
|
+
}
|
|
199
|
+
/** One line fit for the box, or nothing: a long, multi-line or quoted answer is the model not following the brief. */
|
|
200
|
+
export function cleanSuggestion(raw) {
|
|
201
|
+
const text = raw.trim().replace(/^["'“「]+|["'”」]+$/g, "").trim();
|
|
202
|
+
// The model's way of saying there is nothing to offer; an empty reply is
|
|
203
|
+
// the other, but small models will not give one.
|
|
204
|
+
if (!text || /^none\.?$/i.test(text) || text.includes("\n") || text.length > MAX_LENGTH)
|
|
205
|
+
return undefined;
|
|
206
|
+
return text;
|
|
207
|
+
}
|
|
208
|
+
export async function suggestNext(harness, messages, model, signal) {
|
|
209
|
+
const context = suggestContext(messages);
|
|
210
|
+
if (!context)
|
|
211
|
+
return undefined;
|
|
212
|
+
const reply = await harness.generateWith(model, SUGGEST_PROMPT, context, AbortSignal.any([signal, AbortSignal.timeout(TIMEOUT_MS)]), 200);
|
|
213
|
+
return cleanSuggestion(reply);
|
|
214
|
+
}
|
|
215
|
+
//# sourceMappingURL=suggest.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"suggest.js","sourceRoot":"","sources":["../src/suggest.ts"],"names":[],"mappings":"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"}
|
package/dist/terminal.d.ts
CHANGED
|
@@ -1,4 +1,34 @@
|
|
|
1
1
|
import type { ServerResponse } from "node:http";
|
|
2
|
+
/**
|
|
3
|
+
* What the panel says when node-pty will not load. A platform without a
|
|
4
|
+
* prebuilt binary (Linux) needs node-pty's install script to build one, and
|
|
5
|
+
* npm 12 skips dependency install scripts unless they are allowed, so the
|
|
6
|
+
* usual cause has a one-line fix worth saying.
|
|
7
|
+
*/
|
|
8
|
+
export declare function ptyUnavailable(err: Error): string;
|
|
9
|
+
/**
|
|
10
|
+
* Programs a new shell may be opened running, by name. A fixed list rather
|
|
11
|
+
* than a command from the request: the page asks for "claude", and what that
|
|
12
|
+
* means is decided here.
|
|
13
|
+
*
|
|
14
|
+
* `claude` is the hand-off to Claude Code. It is the user's own install,
|
|
15
|
+
* signed in to their own account through Claude Code's own login — OHarness
|
|
16
|
+
* starts it and does nothing else, which is the one way of using a Claude
|
|
17
|
+
* plan from another app that Anthropic's terms leave plainly open. It is
|
|
18
|
+
* typed into the shell rather than spawned in its place, so PATH is the
|
|
19
|
+
* login shell's and the shell is still there when Claude Code exits.
|
|
20
|
+
*/
|
|
21
|
+
export declare const LAUNCHERS: {
|
|
22
|
+
readonly claude: (platform: NodeJS.Platform, resume?: string) => string;
|
|
23
|
+
};
|
|
24
|
+
export type Launcher = keyof typeof LAUNCHERS;
|
|
25
|
+
export declare function isLauncher(value: unknown): value is Launcher;
|
|
26
|
+
/**
|
|
27
|
+
* An agent session id fit to put on a command line: letters, digits and
|
|
28
|
+
* dashes, the shape Claude Code's ids have. Anything else is refused rather
|
|
29
|
+
* than escaped — there is no legitimate id that needs escaping.
|
|
30
|
+
*/
|
|
31
|
+
export declare function isAgentSessionId(value: unknown): value is string;
|
|
2
32
|
export declare class TerminalHub {
|
|
3
33
|
private readonly terminals;
|
|
4
34
|
private seq;
|
|
@@ -8,11 +38,14 @@ export declare class TerminalHub {
|
|
|
8
38
|
name: string;
|
|
9
39
|
exited: boolean;
|
|
10
40
|
}>;
|
|
11
|
-
/**
|
|
41
|
+
/**
|
|
42
|
+
* A new shell in `cwd`; `replace` names one it takes the place of, and
|
|
43
|
+
* `run` a launcher typed into it as soon as it starts.
|
|
44
|
+
*/
|
|
12
45
|
open(cwd: string, size: {
|
|
13
46
|
cols?: unknown;
|
|
14
47
|
rows?: unknown;
|
|
15
|
-
}, replace?: string): Promise<{
|
|
48
|
+
}, replace?: string, run?: Launcher, resume?: string): Promise<{
|
|
16
49
|
id: string;
|
|
17
50
|
name: string;
|
|
18
51
|
} | {
|
package/dist/terminal.js
CHANGED
|
@@ -22,11 +22,25 @@ function loadPty() {
|
|
|
22
22
|
return require(root);
|
|
23
23
|
}
|
|
24
24
|
catch (err) {
|
|
25
|
-
return { error:
|
|
25
|
+
return { error: ptyUnavailable(err) };
|
|
26
26
|
}
|
|
27
27
|
})();
|
|
28
28
|
return loaded;
|
|
29
29
|
}
|
|
30
|
+
/**
|
|
31
|
+
* What the panel says when node-pty will not load. A platform without a
|
|
32
|
+
* prebuilt binary (Linux) needs node-pty's install script to build one, and
|
|
33
|
+
* npm 12 skips dependency install scripts unless they are allowed, so the
|
|
34
|
+
* usual cause has a one-line fix worth saying.
|
|
35
|
+
*/
|
|
36
|
+
export function ptyUnavailable(err) {
|
|
37
|
+
const message = `The embedded terminal is unavailable here: ${err.message}`;
|
|
38
|
+
if (!/pty\.node/.test(err.message))
|
|
39
|
+
return message;
|
|
40
|
+
return (`${message}. Its native part was never built, which happens when npm skips ` +
|
|
41
|
+
"install scripts (npm 12 does unless they are allowed). Reinstall with " +
|
|
42
|
+
"`npm install -g oharness --allow-scripts=node-pty`; building needs Python, make and a C++ compiler.");
|
|
43
|
+
}
|
|
30
44
|
/**
|
|
31
45
|
* node-pty's Unix helper ships in its prebuilds without the execute bit, and
|
|
32
46
|
* without it every spawn fails with a bare "posix_spawnp failed".
|
|
@@ -71,6 +85,40 @@ function shell() {
|
|
|
71
85
|
const file = process.env.SHELL || (process.platform === "darwin" ? "/bin/zsh" : "/bin/bash");
|
|
72
86
|
return { file, args: ["-l"] };
|
|
73
87
|
}
|
|
88
|
+
/**
|
|
89
|
+
* Programs a new shell may be opened running, by name. A fixed list rather
|
|
90
|
+
* than a command from the request: the page asks for "claude", and what that
|
|
91
|
+
* means is decided here.
|
|
92
|
+
*
|
|
93
|
+
* `claude` is the hand-off to Claude Code. It is the user's own install,
|
|
94
|
+
* signed in to their own account through Claude Code's own login — OHarness
|
|
95
|
+
* starts it and does nothing else, which is the one way of using a Claude
|
|
96
|
+
* plan from another app that Anthropic's terms leave plainly open. It is
|
|
97
|
+
* typed into the shell rather than spawned in its place, so PATH is the
|
|
98
|
+
* login shell's and the shell is still there when Claude Code exits.
|
|
99
|
+
*/
|
|
100
|
+
export const LAUNCHERS = {
|
|
101
|
+
claude: (platform, resume) => {
|
|
102
|
+
// `--resume` opens a session `@claude` ran, in Claude Code itself.
|
|
103
|
+
const run = resume ? `claude --resume ${resume}` : "claude";
|
|
104
|
+
return platform === "win32"
|
|
105
|
+
? `if (Get-Command claude -ErrorAction SilentlyContinue) { ${run} } ` +
|
|
106
|
+
"else { Write-Host 'Claude Code is not installed. Get it at https://claude.com/claude-code' -ForegroundColor Yellow }"
|
|
107
|
+
: `if command -v claude >/dev/null 2>&1; then ${run}; ` +
|
|
108
|
+
"else printf '\\033[33mClaude Code is not installed. Get it at https://claude.com/claude-code\\033[0m\\n'; fi";
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
export function isLauncher(value) {
|
|
112
|
+
return typeof value === "string" && Object.hasOwn(LAUNCHERS, value);
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* An agent session id fit to put on a command line: letters, digits and
|
|
116
|
+
* dashes, the shape Claude Code's ids have. Anything else is refused rather
|
|
117
|
+
* than escaped — there is no legitimate id that needs escaping.
|
|
118
|
+
*/
|
|
119
|
+
export function isAgentSessionId(value) {
|
|
120
|
+
return typeof value === "string" && /^[A-Za-z0-9-]{8,64}$/.test(value);
|
|
121
|
+
}
|
|
74
122
|
function clampSize(value, fallback, max) {
|
|
75
123
|
const n = Math.floor(Number(value));
|
|
76
124
|
return Number.isFinite(n) && n > 0 ? Math.min(n, max) : fallback;
|
|
@@ -84,8 +132,11 @@ export class TerminalHub {
|
|
|
84
132
|
.filter((term) => term.cwd === cwd)
|
|
85
133
|
.map((term) => ({ id: term.id, name: term.name, exited: term.exited }));
|
|
86
134
|
}
|
|
87
|
-
/**
|
|
88
|
-
|
|
135
|
+
/**
|
|
136
|
+
* A new shell in `cwd`; `replace` names one it takes the place of, and
|
|
137
|
+
* `run` a launcher typed into it as soon as it starts.
|
|
138
|
+
*/
|
|
139
|
+
async open(cwd, size, replace, run, resume) {
|
|
89
140
|
if (replace)
|
|
90
141
|
this.close(replace);
|
|
91
142
|
const pty = await loadPty();
|
|
@@ -108,7 +159,7 @@ export class TerminalHub {
|
|
|
108
159
|
return { error: err.message };
|
|
109
160
|
}
|
|
110
161
|
const id = `t${++this.seq}`;
|
|
111
|
-
const name = path.basename(file).replace(/\.exe$/i, "");
|
|
162
|
+
const name = run ?? path.basename(file).replace(/\.exe$/i, "");
|
|
112
163
|
const term = { id, cwd, name, pty: proc, backlog: "", listeners: new Set(), exited: false };
|
|
113
164
|
this.terminals.set(id, term);
|
|
114
165
|
proc.onData((data) => {
|
|
@@ -119,6 +170,11 @@ export class TerminalHub {
|
|
|
119
170
|
term.exited = true;
|
|
120
171
|
this.emit(term, { type: "exit", code: exitCode });
|
|
121
172
|
});
|
|
173
|
+
// Typed rather than passed as `-c`, so the shell reads its profile first
|
|
174
|
+
// and is still at its prompt afterwards. The pty buffers it until the
|
|
175
|
+
// shell is ready to read.
|
|
176
|
+
if (run)
|
|
177
|
+
proc.write(LAUNCHERS[run](process.platform, resume && isAgentSessionId(resume) ? resume : undefined) + "\r");
|
|
122
178
|
return { id, name };
|
|
123
179
|
}
|
|
124
180
|
/** Attach a page: what has been written so far, then everything after. */
|
package/dist/terminal.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"terminal.js","sourceRoot":"","sources":["../src/terminal.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AAC/C,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAC5C,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,IAAI,MAAM,WAAW,CAAC;AAqC7B,6EAA6E;AAC7E,MAAM,aAAa,GAAG,GAAG,GAAG,IAAI,CAAC;AAEjC,MAAM,OAAO,GAAG,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAU/C,IAAI,MAA0D,CAAC;AAE/D;;;;;;GAMG;AACH,SAAS,OAAO;IACd,MAAM,KAAK,CAAC,KAAK,IAAI,EAAE;QACrB,IAAI,CAAC;YACH,MAAM,IAAI,GAAG,OAAO,EAAE,CAAC;YACvB,MAAM,sBAAsB,CAAC,IAAI,CAAC,CAAC;YACnC,OAAO,OAAO,CAAC,IAAI,CAAc,CAAC;QACpC,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,OAAO,EAAE,KAAK,EAAE,
|
|
1
|
+
{"version":3,"file":"terminal.js","sourceRoot":"","sources":["../src/terminal.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Images a theme uses, kept where the theme is.
|
|
3
|
+
*
|
|
4
|
+
* A theme never points at a file in a project or at a URL: the image is copied
|
|
5
|
+
* here when the agent proposes it, named by the hash of its bytes, and the
|
|
6
|
+
* theme refers to that name. So a theme keeps working after the project it
|
|
7
|
+
* was made in is moved or deleted, the same picture used twice is stored once,
|
|
8
|
+
* and the name is the proof of the content — `GET /theme-asset/` can serve it
|
|
9
|
+
* without the session token, the way an unguessable capability URL works,
|
|
10
|
+
* because nobody can ask for a file they could not already have.
|
|
11
|
+
*
|
|
12
|
+
* Only still PNG, JPEG and WebP. SVG is a document that can carry script and
|
|
13
|
+
* fetch other things; an animation would run behind every message with no
|
|
14
|
+
* regard for reduced motion.
|
|
15
|
+
*/
|
|
16
|
+
export declare const MAX_ASSET_BYTES: number;
|
|
17
|
+
export declare const assetPath: (id: string) => string;
|
|
18
|
+
export declare const ASSET_TYPES: Record<string, string>;
|
|
19
|
+
/** What the bytes are, from their header — never from the file's name. */
|
|
20
|
+
export declare function sniffImage(bytes: Uint8Array): {
|
|
21
|
+
ext: "png" | "jpg" | "webp";
|
|
22
|
+
} | {
|
|
23
|
+
error: string;
|
|
24
|
+
};
|
|
25
|
+
/** Copy an image file in as an asset; returns its id. */
|
|
26
|
+
export declare function importAsset(file: string): Promise<{
|
|
27
|
+
ok: true;
|
|
28
|
+
id: string;
|
|
29
|
+
} | {
|
|
30
|
+
ok: false;
|
|
31
|
+
error: string;
|
|
32
|
+
}>;
|
|
33
|
+
export declare function assetExists(id: string): Promise<boolean>;
|
|
34
|
+
/**
|
|
35
|
+
* Delete assets nothing refers to any more. Anything touched in the last day
|
|
36
|
+
* is kept: it may belong to a preview card nobody has acted on yet, which only
|
|
37
|
+
* lives in the server's memory.
|
|
38
|
+
*/
|
|
39
|
+
export declare function pruneAssets(keep: Iterable<string>): Promise<void>;
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { mkdir, readFile, readdir, stat, unlink, writeFile } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { userPath } from "@oharness/engine/paths.ts";
|
|
5
|
+
import { ASSET_ID } from "./user-theme.js";
|
|
6
|
+
/**
|
|
7
|
+
* Images a theme uses, kept where the theme is.
|
|
8
|
+
*
|
|
9
|
+
* A theme never points at a file in a project or at a URL: the image is copied
|
|
10
|
+
* here when the agent proposes it, named by the hash of its bytes, and the
|
|
11
|
+
* theme refers to that name. So a theme keeps working after the project it
|
|
12
|
+
* was made in is moved or deleted, the same picture used twice is stored once,
|
|
13
|
+
* and the name is the proof of the content — `GET /theme-asset/` can serve it
|
|
14
|
+
* without the session token, the way an unguessable capability URL works,
|
|
15
|
+
* because nobody can ask for a file they could not already have.
|
|
16
|
+
*
|
|
17
|
+
* Only still PNG, JPEG and WebP. SVG is a document that can carry script and
|
|
18
|
+
* fetch other things; an animation would run behind every message with no
|
|
19
|
+
* regard for reduced motion.
|
|
20
|
+
*/
|
|
21
|
+
export const MAX_ASSET_BYTES = 8 * 1024 * 1024;
|
|
22
|
+
const DIR = () => userPath("theme-assets");
|
|
23
|
+
export const assetPath = (id) => path.join(DIR(), id);
|
|
24
|
+
export const ASSET_TYPES = { png: "image/png", jpg: "image/jpeg", webp: "image/webp" };
|
|
25
|
+
/** What the bytes are, from their header — never from the file's name. */
|
|
26
|
+
export function sniffImage(bytes) {
|
|
27
|
+
const ascii = (from, to) => String.fromCharCode(...bytes.subarray(from, to));
|
|
28
|
+
if (bytes.length > 8 && bytes[0] === 0x89 && ascii(1, 4) === "PNG") {
|
|
29
|
+
// An animated PNG declares itself with an acTL chunk before the first IDAT.
|
|
30
|
+
const head = Buffer.from(bytes.subarray(0, Math.min(bytes.length, 65536)));
|
|
31
|
+
const actl = head.indexOf("acTL"), idat = head.indexOf("IDAT");
|
|
32
|
+
if (actl >= 0 && (idat < 0 || actl < idat))
|
|
33
|
+
return { error: "animated PNGs are not supported" };
|
|
34
|
+
return { ext: "png" };
|
|
35
|
+
}
|
|
36
|
+
if (bytes.length > 3 && bytes[0] === 0xff && bytes[1] === 0xd8 && bytes[2] === 0xff)
|
|
37
|
+
return { ext: "jpg" };
|
|
38
|
+
if (bytes.length > 30 && ascii(0, 4) === "RIFF" && ascii(8, 12) === "WEBP") {
|
|
39
|
+
// Extended WebP carries an animation flag in its VP8X header.
|
|
40
|
+
if (ascii(12, 16) === "VP8X" && (bytes[20] & 0x02) !== 0)
|
|
41
|
+
return { error: "animated WebP is not supported" };
|
|
42
|
+
return { ext: "webp" };
|
|
43
|
+
}
|
|
44
|
+
if (ascii(0, 5).toLowerCase().startsWith("<svg") || ascii(0, 5) === "<?xml")
|
|
45
|
+
return { error: "SVG is not supported; use PNG, JPEG or WebP" };
|
|
46
|
+
if (ascii(0, 3) === "GIF")
|
|
47
|
+
return { error: "GIF is not supported; use PNG, JPEG or WebP" };
|
|
48
|
+
return { error: "not a PNG, JPEG or WebP image" };
|
|
49
|
+
}
|
|
50
|
+
/** Copy an image file in as an asset; returns its id. */
|
|
51
|
+
export async function importAsset(file) {
|
|
52
|
+
let info;
|
|
53
|
+
try {
|
|
54
|
+
info = await stat(file);
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return { ok: false, error: `no file at ${file}` };
|
|
58
|
+
}
|
|
59
|
+
if (!info.isFile())
|
|
60
|
+
return { ok: false, error: `${file} is not a file` };
|
|
61
|
+
if (info.size > MAX_ASSET_BYTES)
|
|
62
|
+
return { ok: false, error: `${path.basename(file)} is over ${MAX_ASSET_BYTES / 1024 / 1024} MB` };
|
|
63
|
+
const bytes = await readFile(file);
|
|
64
|
+
const kind = sniffImage(bytes);
|
|
65
|
+
if ("error" in kind)
|
|
66
|
+
return { ok: false, error: `${path.basename(file)}: ${kind.error}` };
|
|
67
|
+
const id = `${createHash("sha256").update(bytes).digest("hex")}.${kind.ext}`;
|
|
68
|
+
await mkdir(DIR(), { recursive: true });
|
|
69
|
+
await writeFile(assetPath(id), bytes);
|
|
70
|
+
return { ok: true, id };
|
|
71
|
+
}
|
|
72
|
+
export async function assetExists(id) {
|
|
73
|
+
if (!ASSET_ID.test(id))
|
|
74
|
+
return false;
|
|
75
|
+
try {
|
|
76
|
+
return (await stat(assetPath(id))).isFile();
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Delete assets nothing refers to any more. Anything touched in the last day
|
|
84
|
+
* is kept: it may belong to a preview card nobody has acted on yet, which only
|
|
85
|
+
* lives in the server's memory.
|
|
86
|
+
*/
|
|
87
|
+
export async function pruneAssets(keep) {
|
|
88
|
+
const wanted = new Set(keep);
|
|
89
|
+
let names;
|
|
90
|
+
try {
|
|
91
|
+
names = await readdir(DIR());
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
const dayAgo = Date.now() - 24 * 60 * 60 * 1000;
|
|
97
|
+
for (const name of names) {
|
|
98
|
+
if (!ASSET_ID.test(name) || wanted.has(name))
|
|
99
|
+
continue;
|
|
100
|
+
try {
|
|
101
|
+
if ((await stat(assetPath(name))).mtimeMs < dayAgo)
|
|
102
|
+
await unlink(assetPath(name));
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
// Gone already, or not ours to remove: either way nothing to do.
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
//# sourceMappingURL=theme-assets.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"theme-assets.js","sourceRoot":"","sources":["../src/theme-assets.ts"],"names":[],"mappings":"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"}
|