@mjasnikovs/pi-task 0.17.21 → 0.17.23

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.
@@ -65,6 +65,14 @@ export declare class SessionUI {
65
65
  private askLocal;
66
66
  }
67
67
  export declare function publishNotify(message: string, level: 'info' | 'warning' | 'error'): void;
68
+ /**
69
+ * Abort the running agent turn — the browser Stop button. `abort()` lives on the
70
+ * base ExtensionContext ("Abort the current agent operation") so it works for a
71
+ * host chat turn, a /task phase turn, or a /task-auto run alike. The shimmed ctx
72
+ * inherits it from the real event ctx via its prototype, so this is safe even
73
+ * before the user has run a command in the terminal. No-op when nothing is running.
74
+ */
75
+ export declare function interruptAgent(): void;
68
76
  /**
69
77
  * Mirror a task lifecycle notice (the kind pi-task shows on the terminal via
70
78
  * ctx.ui.notify) to connected remote viewers. Task failures and other
@@ -108,6 +108,16 @@ export class SessionUI {
108
108
  export function publishNotify(message, level) {
109
109
  getBridge().broadcast({ type: 'notify', message, level });
110
110
  }
111
+ /**
112
+ * Abort the running agent turn — the browser Stop button. `abort()` lives on the
113
+ * base ExtensionContext ("Abort the current agent operation") so it works for a
114
+ * host chat turn, a /task phase turn, or a /task-auto run alike. The shimmed ctx
115
+ * inherits it from the real event ctx via its prototype, so this is safe even
116
+ * before the user has run a command in the terminal. No-op when nothing is running.
117
+ */
118
+ export function interruptAgent() {
119
+ getBridge().currentCtx?.abort();
120
+ }
111
121
  /**
112
122
  * Mirror a task lifecycle notice (the kind pi-task shows on the terminal via
113
123
  * ctx.ui.notify) to connected remote viewers. Task failures and other
@@ -1,18 +1,24 @@
1
1
  import { setAgentIdle } from './state.js';
2
2
  import { publishNotify } from './bridge.js';
3
- import { agentStart, appendText, textEnd, startTool, updateTool, endTool, agentEnd, addUserTurn, addError, addSystemNote } from './session-state.js';
3
+ import { agentStart, appendText, textEnd, appendThinking, thinkingEnd, startTool, updateTool, endTool, agentEnd, addUserTurn, addError, addSystemNote } from './session-state.js';
4
4
  /** Mirror pi agent events into the authoritative SessionState. Each handler
5
5
  * drives a mutator, which updates the snapshot AND broadcasts the live delta. */
6
6
  export function setupEvents(pi) {
7
- pi.on('agent_start', (_event, _ctx) => {
7
+ pi.on('agent_start', (_event, ctx) => {
8
8
  setAgentIdle(false);
9
- agentStart();
9
+ agentStart(ctx.model?.name);
10
10
  });
11
11
  pi.on('message_update', (event, _ctx) => {
12
12
  const ae = event.assistantMessageEvent;
13
13
  if (ae.type === 'text_delta' && 'delta' in ae && typeof ae.delta === 'string') {
14
14
  appendText(ae.delta);
15
15
  }
16
+ else if (ae.type === 'thinking_delta' && 'delta' in ae && typeof ae.delta === 'string') {
17
+ appendThinking(ae.delta);
18
+ }
19
+ else if (ae.type === 'thinking_end') {
20
+ thinkingEnd();
21
+ }
16
22
  else if (ae.type === 'error') {
17
23
  const errorMessage = 'error' in ae && ae.error && typeof ae.error.errorMessage === 'string' ?
18
24
  ae.error.errorMessage
@@ -43,7 +49,7 @@ export function setupEvents(pi) {
43
49
  });
44
50
  pi.on('agent_end', (_event, ctx) => {
45
51
  setAgentIdle(true);
46
- agentEnd(ctx.getContextUsage());
52
+ agentEnd(ctx.getContextUsage(), ctx.model?.name);
47
53
  // Deliberately no push: agent_end fires on EVERY host turn — every chat
48
54
  // reply, every internal phase turn inside /task, and every internal /task
49
55
  // run inside /task-auto — so a "Task finished" push here floods the device.
@@ -2,6 +2,12 @@ export interface TextPart {
2
2
  kind: 'text';
3
3
  text: string;
4
4
  }
5
+ export interface ThinkingPart {
6
+ kind: 'thinking';
7
+ text: string;
8
+ /** false while thinking deltas are still streaming; true once thinking_end fires. */
9
+ done?: boolean;
10
+ }
5
11
  export interface ToolPart {
6
12
  kind: 'tool';
7
13
  toolCallId: string;
@@ -11,8 +17,10 @@ export interface ToolPart {
11
17
  isError: boolean;
12
18
  /** false while the tool is still running (result not in yet). */
13
19
  done: boolean;
20
+ /** Wall-clock duration once finished, shown dimly in the tool summary. */
21
+ elapsedMs?: number;
14
22
  }
15
- export type Part = TextPart | ToolPart;
23
+ export type Part = TextPart | ThinkingPart | ToolPart;
16
24
  export interface Turn {
17
25
  role: 'user' | 'assistant' | 'system';
18
26
  /** User text, error text, or a system note. Assistant content lives in `parts`. */
@@ -20,6 +28,8 @@ export interface Turn {
20
28
  /** Ordered assistant content (text + tools). */
21
29
  parts?: Part[];
22
30
  error?: boolean;
31
+ /** Epoch ms when the turn was committed — the client renders a dim HH:MM. */
32
+ ts?: number;
23
33
  }
24
34
  export declare class HistoryBuffer {
25
35
  private entries;
@@ -24,7 +24,7 @@ export class HistoryBuffer {
24
24
  return [...this.entries];
25
25
  }
26
26
  _push(entry) {
27
- this.entries.push(entry);
27
+ this.entries.push({ ts: Date.now(), ...entry });
28
28
  if (this.entries.length > this.limit) {
29
29
  this.entries.shift();
30
30
  }
@@ -10,10 +10,27 @@ export interface PromptResolvedMessage {
10
10
  type: 'prompt_resolved';
11
11
  id: string;
12
12
  }
13
- /** The single task-widget slot. `lines: null` clears it. */
13
+ /** Structured task-widget payload lets the browser render a real progress bar,
14
+ * phase badge and elapsed clock instead of reflowing the terminal's text lines.
15
+ * Always sent ALONGSIDE `lines`, which stays the authoritative fallback. */
16
+ export interface WidgetData {
17
+ /** Head line, e.g. "TASK_0003 · auth routes" or "/task-auto · building". */
18
+ title: string;
19
+ /** Current phase/stage label, e.g. "refine", "implementing", "enforcing guidelines". */
20
+ phase: string;
21
+ /** Progress numerator (current step) — omitted for stages without numbering. */
22
+ done?: number;
23
+ /** Progress denominator (total steps) — omitted for stages without numbering. */
24
+ total?: number;
25
+ /** Pre-formatted elapsed clock, e.g. "2:14". */
26
+ elapsed?: string;
27
+ }
28
+ /** The single task-widget slot. `lines: null` clears it. `data` carries the
29
+ * structured view (null when clearing or when a producer only has text). */
14
30
  export interface WidgetMessage {
15
31
  type: 'widget';
16
32
  lines: string[] | null;
33
+ data?: WidgetData | null;
17
34
  }
18
35
  export interface NotifyMessage {
19
36
  type: 'notify';
@@ -58,5 +75,9 @@ export interface ClientPromptAnswer {
58
75
  id: string;
59
76
  value: string | undefined;
60
77
  }
61
- export type ClientMessage = ClientChatMessage | ClientPromptAnswer;
78
+ /** Stop the running agent turn (the browser Stop button). */
79
+ export interface ClientInterrupt {
80
+ type: 'interrupt';
81
+ }
82
+ export type ClientMessage = ClientChatMessage | ClientPromptAnswer | ClientInterrupt;
62
83
  export declare function isClientMessage(x: unknown): x is ClientMessage;
@@ -6,6 +6,8 @@ export function isClientMessage(x) {
6
6
  const m = x;
7
7
  if (m.type === 'message')
8
8
  return typeof m.text === 'string';
9
+ if (m.type === 'interrupt')
10
+ return true;
9
11
  if (m.type === 'prompt_answer') {
10
12
  return typeof m.id === 'string' && (m.value === undefined || typeof m.value === 'string');
11
13
  }
@@ -1,5 +1,5 @@
1
1
  import { getConfig } from '../config/config.js';
2
- import { getBridge, dispatchRemoteLine, dispatchRemoteNewSession, makeShimmedCtx } from './bridge.js';
2
+ import { getBridge, dispatchRemoteLine, dispatchRemoteNewSession, makeShimmedCtx, interruptAgent } from './bridge.js';
3
3
  import { setupEvents } from './events.js';
4
4
  import { reset, addUserTurn } from './session-state.js';
5
5
  import { html } from './ui.js';
@@ -33,11 +33,15 @@ export function registerRemote(pi) {
33
33
  S.send?.(plain);
34
34
  }
35
35
  else {
36
- S.send?.(plain, { deliverAs: 'followUp' });
36
+ // Mid-run: steer the live turn (inject the message into the
37
+ // current generation) rather than queueing it for after, so a
38
+ // remote nudge lands immediately — matching the composer's
39
+ // "delivered mid-run" affordance.
40
+ S.send?.(plain, { deliverAs: 'steer' });
37
41
  }
38
42
  }
39
43
  });
40
- }, wsUrl => html(wsUrl));
44
+ }, wsUrl => html(wsUrl), interruptAgent);
41
45
  // Hands-off HTTPS: point Tailscale serve at our port so phones get a
42
46
  // secure context. Best-effort — any failure degrades to the http URL.
43
47
  S.serveResult = await ensureTailscaleServe(S.server.port).catch(() => ({ state: 'unavailable' }));
@@ -28,5 +28,5 @@ export declare function getLocalIPs(nets?: NodeJS.Dict<import("node:os").Network
28
28
  * Tailscale line uses the MagicDNS host when known (resolves to the same node,
29
29
  * but is what SSH and webpush certs need), falling back to the raw IP. */
30
30
  export declare function formatAddresses(ips: LocalIPs, port: number, tsHost?: string): AddressLine[];
31
- export declare function startServer(onMessage: MessageCallback, getHtml: (wsUrl: string) => string): Promise<ServerHandle>;
31
+ export declare function startServer(onMessage: MessageCallback, getHtml: (wsUrl: string) => string, onInterrupt?: () => void): Promise<ServerHandle>;
32
32
  export {};
@@ -64,7 +64,7 @@ async function findPort(start, max) {
64
64
  }
65
65
  throw new Error(`No free port found in range ${start}–${start + max - 1}`);
66
66
  }
67
- export async function startServer(onMessage, getHtml) {
67
+ export async function startServer(onMessage, getHtml, onInterrupt) {
68
68
  const port = await findPort(8800, 100);
69
69
  const ips = getLocalIPs();
70
70
  const ip = ips.primary;
@@ -159,6 +159,10 @@ export async function startServer(onMessage, getHtml) {
159
159
  }
160
160
  if (!isClientMessage(msg))
161
161
  return;
162
+ if (msg.type === 'interrupt') {
163
+ onInterrupt?.();
164
+ return;
165
+ }
162
166
  if (msg.type === 'prompt_answer') {
163
167
  answerPrompt(msg.id, msg.value);
164
168
  return;
@@ -1,6 +1,6 @@
1
1
  import { HistoryBuffer } from './history.js';
2
2
  import type { Turn, Part } from './history.js';
3
- import type { ContextUsage, PromptMessage } from './protocol.js';
3
+ import type { ContextUsage, PromptMessage, WidgetData } from './protocol.js';
4
4
  export interface LiveTurn {
5
5
  /** Ordered assistant content (text segments + tool calls) for this run. */
6
6
  parts: Part[];
@@ -14,36 +14,50 @@ export interface SnapshotMessage {
14
14
  live: LiveTurn | null;
15
15
  agentRunning: boolean;
16
16
  taskWidget: string[] | null;
17
+ taskWidgetData: WidgetData | null;
17
18
  prompt: PromptMessage | null;
18
19
  context: ContextUsage | null;
20
+ model: string | null;
19
21
  }
20
22
  interface SessionState {
21
23
  history: HistoryBuffer;
22
24
  live: LiveTurn | null;
23
25
  agentRunning: boolean;
24
26
  taskWidget: string[] | null;
27
+ taskWidgetData: WidgetData | null;
25
28
  prompt: PromptMessage | null;
26
29
  context: ContextUsage | null;
30
+ /** Human-readable active model name (e.g. "Qwen3.6 27B"), for the header chip. */
31
+ model: string | null;
32
+ /** tool start timestamps (ms), keyed by toolCallId — kept off the serialized
33
+ * parts so it never reaches the client; used only to compute elapsedMs. */
34
+ toolStarts: Record<string, number>;
27
35
  /** Broadcast sink — swapped in tests via _setSink. */
28
36
  sink: (msg: unknown) => void;
29
37
  }
30
38
  export declare function getState(): SessionState;
31
39
  /** @internal Test-only: swap the broadcast sink to capture emitted deltas. */
32
40
  export declare function _setSink(fn: (msg: unknown) => void): void;
33
- export declare function agentStart(): void;
41
+ export declare function agentStart(model?: string): void;
34
42
  export declare function appendText(delta: string): void;
35
43
  export declare function textEnd(): void;
44
+ /** Accumulate a reasoning/thinking delta into the current thinking part (a new
45
+ * part opens when the previous one is closed or the last part isn't thinking).
46
+ * Stored as a part kind so a reconnect snapshot replays the collapsed block. */
47
+ export declare function appendThinking(delta: string): void;
48
+ export declare function thinkingEnd(): void;
36
49
  export declare function startTool(toolCallId: string, toolName: string, args: unknown): void;
37
50
  export declare function updateTool(toolCallId: string, partialResult: unknown): void;
38
51
  export declare function endTool(toolCallId: string, toolName: string, result: unknown, isError: boolean): void;
39
- export declare function agentEnd(context: ContextUsage): void;
52
+ export declare function agentEnd(context: ContextUsage, model?: string): void;
40
53
  export declare function addUserTurn(text: string): void;
41
54
  export declare function addError(message: string): void;
42
55
  /** A persistent inline note (committed to the transcript so it survives reconnect)
43
56
  * plus a live delta so connected clients render it immediately. */
44
57
  export declare function addSystemNote(text: string): void;
45
- /** The single task-widget slot. Empty/undefined lines clear it. */
46
- export declare function setTaskWidget(lines: string[] | null | undefined): void;
58
+ /** The single task-widget slot. Empty/undefined lines clear it. `data` is the
59
+ * structured view rendered by the browser; the lines remain the fallback. */
60
+ export declare function setTaskWidget(lines: string[] | null | undefined, data?: WidgetData | null): void;
47
61
  export declare function setPrompt(prompt: PromptMessage): void;
48
62
  export declare function clearPrompt(id: string): void;
49
63
  export declare function setContext(context: ContextUsage): void;
@@ -18,8 +18,11 @@ function fresh() {
18
18
  live: null,
19
19
  agentRunning: false,
20
20
  taskWidget: null,
21
+ taskWidgetData: null,
21
22
  prompt: null,
22
23
  context: null,
24
+ model: null,
25
+ toolStarts: {},
23
26
  sink: wsBroadcast
24
27
  };
25
28
  }
@@ -38,11 +41,13 @@ function ensureLive(s) {
38
41
  return s.live;
39
42
  }
40
43
  // ─── Mutators ────────────────────────────────────────────────────────────────
41
- export function agentStart() {
44
+ export function agentStart(model) {
42
45
  const s = getState();
43
46
  s.live = { parts: [], textOpen: false };
44
47
  s.agentRunning = true;
45
- s.sink({ type: 'agent_start' });
48
+ if (model)
49
+ s.model = model;
50
+ s.sink({ type: 'agent_start', model: s.model });
46
51
  }
47
52
  export function appendText(delta) {
48
53
  const s = getState();
@@ -63,6 +68,29 @@ export function textEnd() {
63
68
  s.live.textOpen = false; // next text starts a new segment
64
69
  s.sink({ type: 'text_end' });
65
70
  }
71
+ /** Accumulate a reasoning/thinking delta into the current thinking part (a new
72
+ * part opens when the previous one is closed or the last part isn't thinking).
73
+ * Stored as a part kind so a reconnect snapshot replays the collapsed block. */
74
+ export function appendThinking(delta) {
75
+ const s = getState();
76
+ const live = ensureLive(s);
77
+ const last = live.parts[live.parts.length - 1];
78
+ if (last && last.kind === 'thinking' && !last.done) {
79
+ last.text += delta;
80
+ }
81
+ else {
82
+ live.parts.push({ kind: 'thinking', text: delta, done: false });
83
+ }
84
+ live.textOpen = false; // a following text delta begins a fresh bubble
85
+ s.sink({ type: 'thinking_delta', delta });
86
+ }
87
+ export function thinkingEnd() {
88
+ const s = getState();
89
+ const last = s.live?.parts[s.live.parts.length - 1];
90
+ if (last && last.kind === 'thinking')
91
+ last.done = true;
92
+ s.sink({ type: 'thinking_end' });
93
+ }
66
94
  export function startTool(toolCallId, toolName, args) {
67
95
  const s = getState();
68
96
  const live = ensureLive(s);
@@ -76,6 +104,7 @@ export function startTool(toolCallId, toolName, args) {
76
104
  isError: false,
77
105
  done: false
78
106
  });
107
+ s.toolStarts[toolCallId] = Date.now();
79
108
  s.sink({ type: 'tool_start', toolCallId, toolName, args });
80
109
  }
81
110
  export function updateTool(toolCallId, partialResult) {
@@ -85,10 +114,18 @@ export function endTool(toolCallId, toolName, result, isError) {
85
114
  const s = getState();
86
115
  const live = ensureLive(s);
87
116
  const part = live.parts.find((p) => p.kind === 'tool' && p.toolCallId === toolCallId);
117
+ const startedAt = s.toolStarts[toolCallId];
118
+ let elapsedMs;
119
+ if (startedAt != null) {
120
+ elapsedMs = Date.now() - startedAt;
121
+ delete s.toolStarts[toolCallId];
122
+ }
88
123
  if (part) {
89
124
  part.result = result;
90
125
  part.isError = isError;
91
126
  part.done = true;
127
+ if (elapsedMs != null)
128
+ part.elapsedMs = elapsedMs;
92
129
  }
93
130
  else {
94
131
  // tool_end without a matching start (shouldn't happen) — append a done tool.
@@ -102,16 +139,18 @@ export function endTool(toolCallId, toolName, result, isError) {
102
139
  done: true
103
140
  });
104
141
  }
105
- s.sink({ type: 'tool_end', toolCallId, toolName, result, isError });
142
+ s.sink({ type: 'tool_end', toolCallId, toolName, result, isError, elapsedMs });
106
143
  }
107
- export function agentEnd(context) {
144
+ export function agentEnd(context, model) {
108
145
  const s = getState();
109
146
  if (s.live)
110
147
  s.history.addAssistantTurn(s.live.parts);
111
148
  s.live = null;
112
149
  s.agentRunning = false;
113
150
  s.context = context;
114
- s.sink({ type: 'agent_end', contextUsage: context });
151
+ if (model)
152
+ s.model = model;
153
+ s.sink({ type: 'agent_end', contextUsage: context, model: s.model });
115
154
  }
116
155
  export function addUserTurn(text) {
117
156
  const s = getState();
@@ -132,11 +171,13 @@ export function addSystemNote(text) {
132
171
  s.history.addSystemNote(text);
133
172
  s.sink({ type: 'system_note', text });
134
173
  }
135
- /** The single task-widget slot. Empty/undefined lines clear it. */
136
- export function setTaskWidget(lines) {
174
+ /** The single task-widget slot. Empty/undefined lines clear it. `data` is the
175
+ * structured view rendered by the browser; the lines remain the fallback. */
176
+ export function setTaskWidget(lines, data) {
137
177
  const s = getState();
138
178
  s.taskWidget = lines && lines.length ? lines : null;
139
- s.sink({ type: 'widget', lines: s.taskWidget });
179
+ s.taskWidgetData = s.taskWidget ? (data ?? null) : null;
180
+ s.sink({ type: 'widget', lines: s.taskWidget, data: s.taskWidgetData });
140
181
  }
141
182
  export function setPrompt(prompt) {
142
183
  const s = getState();
@@ -160,8 +201,11 @@ export function reset() {
160
201
  s.live = null;
161
202
  s.agentRunning = false;
162
203
  s.taskWidget = null;
204
+ s.taskWidgetData = null;
163
205
  s.prompt = null;
164
206
  s.context = null;
207
+ // Deliberately keep s.model: the active model doesn't change across a /new,
208
+ // so the header chip needn't blank and re-fill on every session switch.
165
209
  s.sink({ type: 'reset' });
166
210
  }
167
211
  /** Serialize the whole state for a (re)connecting client. */
@@ -173,7 +217,9 @@ export function snapshot() {
173
217
  live: s.live ? { parts: [...s.live.parts], textOpen: s.live.textOpen } : null,
174
218
  agentRunning: s.agentRunning,
175
219
  taskWidget: s.taskWidget,
220
+ taskWidgetData: s.taskWidgetData,
176
221
  prompt: s.prompt,
177
- context: s.context
222
+ context: s.context,
223
+ model: s.model
178
224
  };
179
225
  }
@@ -0,0 +1,2 @@
1
+ /** Emitted client JS (top-level function declarations + language tables). */
2
+ export declare function highlightModule(): string;
@@ -0,0 +1,119 @@
1
+ // Generalized single-pass syntax highlighter, shipped as a JS string and
2
+ // concatenated into the page by ui.ts. It grew out of the client's original
3
+ // JS-only tokenizer: the string/comment/number/identifier logic is now shared and
4
+ // parameterized by a per-language config (keyword set + comment markers + string
5
+ // flavors). Unknown languages fall back to escaped plain text (unchanged).
6
+ //
7
+ // Depends on escHtml (ui-render.ts) — both land in the same <script>.
8
+ /** Emitted client JS (top-level function declarations + language tables). */
9
+ export function highlightModule() {
10
+ return ` var HL_JS_KW = new Set(('break case catch class const continue debugger default delete do else export '
11
+ + 'extends finally for from function if import in instanceof let new of return static super switch this '
12
+ + 'throw try typeof var void while with yield async await type interface enum implements abstract as declare '
13
+ + 'namespace readonly undefined null true false override satisfies public private protected get set').split(' '));
14
+ var HL_PY_KW = new Set(('def return if elif else for while in not and or import from as class try except finally '
15
+ + 'raise with lambda yield pass break continue global nonlocal assert del None True False async await is print self').split(' '));
16
+ var HL_SH_KW = new Set(('if then else elif fi for while do done case esac in function return echo export local read '
17
+ + 'cd set unset source alias exit test').split(' '));
18
+ var HL_SQL_KW = new Set(('select from where insert into values update set delete create table drop alter add column '
19
+ + 'join left right inner outer full on group by order having limit offset union and or not null is as distinct '
20
+ + 'count sum avg min max primary key foreign references index unique default int integer text varchar char boolean '
21
+ + 'timestamp date time serial begin commit rollback').split(' '));
22
+ var HL_GO_KW = new Set(('func package import var const type struct interface map chan go defer return if else for range '
23
+ + 'switch case default break continue select nil true false make new append len cap string int int64 float64 bool byte rune error').split(' '));
24
+ var HL_RUST_KW = new Set(('fn let mut const static struct enum impl trait pub use mod match if else for while loop return '
25
+ + 'break continue self Self super crate as where move ref dyn async await unsafe true false None Some Ok Err String Vec Option Result Box').split(' '));
26
+ var HL_YAML_KW = new Set('true false null yes no on off'.split(' '));
27
+ var HL_JSON_KW = new Set('true false null'.split(' '));
28
+
29
+ var HL_LANGS = {
30
+ js: {kw: HL_JS_KW, line: '//', block: ['/*', '*/'], tmpl: true},
31
+ ts: {kw: HL_JS_KW, line: '//', block: ['/*', '*/'], tmpl: true},
32
+ python: {kw: HL_PY_KW, line: '#', block: null, triple: true},
33
+ sh: {kw: HL_SH_KW, line: '#', block: null},
34
+ sql: {kw: HL_SQL_KW, line: '--', block: ['/*', '*/'], ci: true},
35
+ go: {kw: HL_GO_KW, line: '//', block: ['/*', '*/'], tmpl: true},
36
+ rust: {kw: HL_RUST_KW, line: '//', block: ['/*', '*/']},
37
+ yaml: {kw: HL_YAML_KW, line: '#', block: null},
38
+ json: {kw: HL_JSON_KW, line: null, block: null},
39
+ css: {kw: null, line: null, block: ['/*', '*/']}
40
+ };
41
+ var HL_ALIAS = {javascript: 'js', jsx: 'js', mjs: 'js', cjs: 'js', node: 'js', typescript: 'ts', tsx: 'ts',
42
+ py: 'python', python3: 'python', bash: 'sh', shell: 'sh', shellscript: 'sh', zsh: 'sh', console: 'sh',
43
+ yml: 'yaml', rs: 'rust', golang: 'go', htm: 'html', xml: 'html', svg: 'html', scss: 'css'};
44
+
45
+ function hlSpan(cls, text) { return '<span class="' + cls + '">' + escHtml(text) + '</span>'; }
46
+
47
+ function tokenize(code, cfg) {
48
+ var BT = String.fromCharCode(96);
49
+ var r = '', i = 0, n = code.length;
50
+ while (i < n) {
51
+ var ch = code[i];
52
+ if (cfg.block && code.startsWith(cfg.block[0], i)) {
53
+ var end = code.indexOf(cfg.block[1], i + cfg.block[0].length);
54
+ var je = end < 0 ? n : end + cfg.block[1].length;
55
+ r += hlSpan('hl-cmt', code.slice(i, je)); i = je; continue;
56
+ }
57
+ if (cfg.line && code.startsWith(cfg.line, i)) {
58
+ var jl = i; while (jl < n && code[jl] !== '\\n') jl++;
59
+ r += hlSpan('hl-cmt', code.slice(i, jl)); i = jl; continue;
60
+ }
61
+ if (ch === '"' || ch === "'" || (cfg.tmpl && ch === BT)) {
62
+ if (cfg.triple && code[i + 1] === ch && code[i + 2] === ch) {
63
+ var close = code.indexOf(ch + ch + ch, i + 3);
64
+ var jt = close < 0 ? n : close + 3;
65
+ r += hlSpan('hl-str', code.slice(i, jt)); i = jt; continue;
66
+ }
67
+ var js = i + 1;
68
+ while (js < n) { if (code[js] === '\\\\') { js += 2; continue; } if (code[js] === ch || code[js] === '\\n') break; js++; }
69
+ if (code[js] === ch) js++;
70
+ r += hlSpan('hl-str', code.slice(i, js)); i = js; continue;
71
+ }
72
+ if (ch >= '0' && ch <= '9') {
73
+ var jn = i;
74
+ if (code[i] === '0' && /[xXoObB]/.test(code[i + 1] || '')) {
75
+ jn += 2; while (jn < n && /[0-9a-fA-F_]/.test(code[jn])) jn++;
76
+ } else {
77
+ while (jn < n && ((code[jn] >= '0' && code[jn] <= '9') || code[jn] === '_')) jn++;
78
+ if (code[jn] === '.') { jn++; while (jn < n && code[jn] >= '0' && code[jn] <= '9') jn++; }
79
+ if (code[jn] === 'e' || code[jn] === 'E') { jn++; if (code[jn] === '+' || code[jn] === '-') jn++; while (jn < n && code[jn] >= '0' && code[jn] <= '9') jn++; }
80
+ if (code[jn] === 'n') jn++;
81
+ }
82
+ r += hlSpan('hl-num', code.slice(i, jn)); i = jn; continue;
83
+ }
84
+ if (/[a-zA-Z_$]/.test(ch)) {
85
+ var jw = i; while (jw < n && /[a-zA-Z0-9_$]/.test(code[jw])) jw++;
86
+ var word = code.slice(i, jw);
87
+ var kwHit = cfg.kw && (cfg.kw.has(word) || (cfg.ci && cfg.kw.has(word.toLowerCase())));
88
+ if (kwHit) r += hlSpan('hl-kw', word);
89
+ else if (code[jw] === '(') r += hlSpan('hl-fn', word);
90
+ else r += escHtml(word);
91
+ i = jw; continue;
92
+ }
93
+ r += escHtml(ch); i++;
94
+ }
95
+ return r;
96
+ }
97
+
98
+ // Markup languages don't fit the identifier tokenizer; highlight tag names and
99
+ // attribute names off the already-escaped text.
100
+ function highlightMarkup(code) {
101
+ var e = escHtml(code);
102
+ // Attribute names first (before any spans exist), then tag names — otherwise
103
+ // the attribute pass would re-match the class="" inside the spans it inserted.
104
+ e = e.replace(/([a-zA-Z-]+)=("|&quot;)/g, function (_, a, q) { return '<span class="hl-fn">' + a + '</span>=' + q; });
105
+ e = e.replace(/(&lt;\\/?)([a-zA-Z][\\w-]*)/g, function (_, p, t) { return p + '<span class="hl-kw">' + t + '</span>'; });
106
+ return e;
107
+ }
108
+
109
+ function syntaxHighlight(code, lang) {
110
+ code = String(code == null ? '' : code);
111
+ lang = (lang || '').toLowerCase();
112
+ lang = HL_ALIAS[lang] || lang;
113
+ if (lang === 'html') return highlightMarkup(code);
114
+ var cfg = HL_LANGS[lang];
115
+ if (!cfg) return escHtml(code);
116
+ return tokenize(code, cfg);
117
+ }
118
+ `;
119
+ }
@@ -0,0 +1,2 @@
1
+ /** Emitted client JS (top-level function declarations). */
2
+ export declare function renderModule(): string;