@gavin-hu/my-pi-agent 0.8.0 → 0.9.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/README.md +9 -8
- package/README.zh-CN.md +8 -8
- package/assets/file-browse.png +0 -0
- package/assets/plan-review.png +0 -0
- package/extensions/bytefly/README.md +49 -36
- package/extensions/bytefly/doctor/format.ts +36 -10
- package/extensions/bytefly/doctor/tools.ts +6 -5
- package/extensions/bytefly/footer/config.ts +1 -1
- package/extensions/bytefly/footer/render/format.ts +5 -18
- package/extensions/bytefly/footer/render/layout.ts +2 -1
- package/extensions/bytefly/footer/render/segments.ts +51 -78
- package/extensions/bytefly/header/component.ts +33 -16
- package/extensions/bytefly/header/config.ts +28 -18
- package/extensions/bytefly/index.ts +1 -1
- package/extensions/bytefly/panel/commands.ts +69 -35
- package/extensions/bytefly/panel/format.ts +8 -8
- package/extensions/bytefly/panel/tui.ts +45 -70
- package/extensions/bytefly/separator/config.ts +16 -8
- package/extensions/bytefly/separator/format.ts +13 -40
- package/extensions/bytefly/separator/types.ts +0 -10
- package/extensions/document/README.md +1 -1
- package/extensions/document/render.ts +11 -10
- package/extensions/document/tool.ts +1 -1
- package/extensions/file-browse/README.md +62 -14
- package/extensions/file-browse/client.ts +103 -2
- package/extensions/file-browse/config.ts +12 -0
- package/extensions/file-browse/diff.ts +187 -0
- package/extensions/file-browse/git.ts +176 -1
- package/extensions/file-browse/html.ts +412 -23
- package/extensions/file-browse/index.ts +15 -4
- package/extensions/file-browse/markdown.ts +98 -5
- package/extensions/file-browse/router.ts +94 -2
- package/extensions/file-browse/server.ts +4 -1
- package/extensions/goal/format.ts +11 -10
- package/extensions/goal/tools.ts +6 -5
- package/extensions/goal/tui.ts +1 -1
- package/extensions/job/README.md +2 -2
- package/extensions/job/format.ts +36 -13
- package/extensions/job/index.ts +1 -1
- package/extensions/job/tools.ts +11 -10
- package/extensions/job/tui.ts +141 -220
- package/extensions/job/ui.ts +24 -49
- package/extensions/memory/README.md +1 -1
- package/extensions/memory/tools.ts +4 -3
- package/extensions/memory/tui.ts +32 -36
- package/extensions/plan/README.md +64 -34
- package/extensions/plan/blocks.ts +44 -2
- package/extensions/plan/feedback.ts +20 -5
- package/extensions/plan/index.ts +1 -1
- package/extensions/plan/runtime.ts +3 -2
- package/extensions/plan/tools.ts +23 -22
- package/extensions/plan/tui.ts +21 -27
- package/extensions/plan/web-client.ts +335 -30
- package/extensions/plan/web-page.ts +110 -30
- package/extensions/plan/web-router.ts +6 -4
- package/extensions/plan/web.ts +41 -10
- package/extensions/question/README.md +1 -1
- package/extensions/question/tools.ts +12 -11
- package/extensions/question/tui.ts +132 -76
- package/extensions/rewind/README.md +1 -1
- package/extensions/rewind/format.ts +29 -1
- package/extensions/rewind/rewind-tui.ts +66 -62
- package/extensions/rewind/status.ts +3 -2
- package/extensions/subagent/render.ts +62 -47
- package/extensions/todo/README.md +2 -2
- package/extensions/todo/format.ts +31 -10
- package/extensions/todo/tools.ts +5 -4
- package/extensions/todo/tui.ts +45 -26
- package/extensions/watch/README.md +2 -2
- package/extensions/watch/format.ts +33 -13
- package/extensions/watch/index.ts +1 -1
- package/extensions/watch/tools.ts +12 -7
- package/extensions/watch/tui.ts +135 -194
- package/extensions/watch/ui.ts +24 -46
- package/extensions/web-access/fetch/tool.ts +12 -11
- package/extensions/web-access/search/tool.ts +16 -13
- package/extensions/wechat/README.md +2 -2
- package/extensions/wechat/index.ts +4 -3
- package/extensions/wechat/login-screen.ts +8 -5
- package/extensions/wechat/status-screen.ts +41 -22
- package/extensions/wechat/tools.ts +6 -5
- package/extensions/worktree/enter.ts +1 -1
- package/extensions/worktree/index.ts +1 -1
- package/extensions/worktree/runtime.ts +1 -1
- package/lib/README.md +10 -7
- package/lib/requirements.ts +2 -2
- package/lib/{tui.ts → tui/chrome.ts} +9 -30
- package/lib/tui/columns.ts +96 -0
- package/lib/{list-cursor.ts → tui/cursor.ts} +56 -3
- package/lib/tui/gauge.ts +78 -0
- package/lib/tui/index.ts +25 -0
- package/lib/tui/line-pane.ts +173 -0
- package/lib/tui/rows.ts +109 -0
- package/lib/tui/rules.ts +88 -0
- package/lib/tui/status-controller.ts +107 -0
- package/lib/tui/tokens.ts +58 -0
- package/package.json +2 -2
- /package/lib/{ui.ts → tui/glyphs.ts} +0 -0
|
@@ -11,8 +11,9 @@
|
|
|
11
11
|
|
|
12
12
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
13
13
|
import { formatTokens } from "../../../lib/format.ts";
|
|
14
|
-
import { reuseText } from "../../../lib/tui.ts";
|
|
15
|
-
import {
|
|
14
|
+
import { reuseText } from "../../../lib/tui/chrome.ts";
|
|
15
|
+
import { paint } from "../../../lib/tui/tokens.ts";
|
|
16
|
+
import { SEPARATORS } from "../../../lib/tui/glyphs.ts";
|
|
16
17
|
import { charLength } from "../text.ts";
|
|
17
18
|
import { oneLine } from "../transcript.ts";
|
|
18
19
|
import { createPageCache, type PageCache } from "./cache.ts";
|
|
@@ -204,9 +205,9 @@ export function registerFetchTool(pi: ExtensionAPI, deps: FetchToolDeps = {}): v
|
|
|
204
205
|
renderCall(args, theme, context) {
|
|
205
206
|
const { url, find, startIndex } = args as WebFetchArgs;
|
|
206
207
|
const text = reuseText(context?.lastComponent);
|
|
207
|
-
let line = theme.fg("toolTitle", theme.bold(`${TOOL_NAME} `)) + theme
|
|
208
|
-
if (find?.length) line += theme
|
|
209
|
-
else if (startIndex) line += theme
|
|
208
|
+
let line = theme.fg("toolTitle", theme.bold(`${TOOL_NAME} `)) + paint(theme, "accent", oneLine(url ?? ""));
|
|
209
|
+
if (find?.length) line += paint(theme, "faint", ` find ${find.map((f) => `"${oneLine(f)}"`).join(", ")}`);
|
|
210
|
+
else if (startIndex) line += paint(theme, "faint", ` (from ${startIndex})`);
|
|
210
211
|
text.setText(line);
|
|
211
212
|
return text;
|
|
212
213
|
},
|
|
@@ -216,21 +217,21 @@ export function registerFetchTool(pi: ExtensionAPI, deps: FetchToolDeps = {}): v
|
|
|
216
217
|
const page = result.details as FetchResponse | undefined;
|
|
217
218
|
if (page?.error) {
|
|
218
219
|
const message = oneLine(page.error);
|
|
219
|
-
text.setText(theme
|
|
220
|
+
text.setText(paint(theme, "bad", message.startsWith("Error:") ? message : `Error: ${message}`));
|
|
220
221
|
return text;
|
|
221
222
|
}
|
|
222
223
|
if (!page || result.isError) {
|
|
223
224
|
const first = result.content[0];
|
|
224
225
|
const message = oneLine(first?.type === "text" ? first.text : "Fetch failed");
|
|
225
|
-
text.setText(theme
|
|
226
|
+
text.setText(paint(theme, "bad", message.startsWith("Error:") ? message : `Error: ${message}`));
|
|
226
227
|
return text;
|
|
227
228
|
}
|
|
228
229
|
|
|
229
230
|
const label = oneLine(page.title || page.finalUrl);
|
|
230
|
-
let line = theme
|
|
231
|
-
if (page.matches.length > 0) line += theme
|
|
232
|
-
else line += theme
|
|
233
|
-
if (page.cached) line += theme
|
|
231
|
+
let line = paint(theme, "meta", label);
|
|
232
|
+
if (page.matches.length > 0) line += paint(theme, "faint", `${SEPARATORS.item}${page.matches.length} match(es)`);
|
|
233
|
+
else line += paint(theme, "faint", `${SEPARATORS.item}${formatTokens(page.totalChars)} chars`);
|
|
234
|
+
if (page.cached) line += paint(theme, "faint", " (cached)");
|
|
234
235
|
text.setText(line);
|
|
235
236
|
return text;
|
|
236
237
|
},
|
|
@@ -10,8 +10,9 @@
|
|
|
10
10
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
11
11
|
import { loadSearchConfig } from "../config.ts";
|
|
12
12
|
import { oneLine } from "../transcript.ts";
|
|
13
|
-
import { reuseText } from "../../../lib/tui.ts";
|
|
14
|
-
import {
|
|
13
|
+
import { reuseText } from "../../../lib/tui/chrome.ts";
|
|
14
|
+
import { paint } from "../../../lib/tui/tokens.ts";
|
|
15
|
+
import { expandHint, SEPARATORS } from "../../../lib/tui/glyphs.ts";
|
|
15
16
|
import { formatResults } from "./format.ts";
|
|
16
17
|
import { createThrottle, runSearch, type SearchDeps } from "./search.ts";
|
|
17
18
|
import { resolveRequest, WebSearchOutput, WebSearchParams, type SearchResponse, type WebSearchArgs } from "./schema.ts";
|
|
@@ -68,8 +69,8 @@ export function registerSearchTool(pi: ExtensionAPI, deps: SearchDeps = {}): voi
|
|
|
68
69
|
renderCall(args, theme, context) {
|
|
69
70
|
const { query, maxResults } = args as WebSearchArgs;
|
|
70
71
|
const text = reuseText(context?.lastComponent);
|
|
71
|
-
let line = theme.fg("toolTitle", theme.bold(`${TOOL_NAME} `)) + theme
|
|
72
|
-
if (maxResults) line += theme
|
|
72
|
+
let line = theme.fg("toolTitle", theme.bold(`${TOOL_NAME} `)) + paint(theme, "accent", oneLine(query ?? ""));
|
|
73
|
+
if (maxResults) line += paint(theme, "faint", ` (${maxResults} results)`);
|
|
73
74
|
text.setText(line);
|
|
74
75
|
return text;
|
|
75
76
|
},
|
|
@@ -80,11 +81,11 @@ export function registerSearchTool(pi: ExtensionAPI, deps: SearchDeps = {}): voi
|
|
|
80
81
|
if (!details || result.isError) {
|
|
81
82
|
const first = result.content[0];
|
|
82
83
|
const message = first?.type === "text" ? first.text : "Search failed";
|
|
83
|
-
text.setText(theme
|
|
84
|
+
text.setText(paint(theme, "bad", `Error: ${oneLine(message)}`));
|
|
84
85
|
return text;
|
|
85
86
|
}
|
|
86
87
|
if (details.provider === "none") {
|
|
87
|
-
text.setText(theme
|
|
88
|
+
text.setText(paint(theme, "faint", `No results for "${oneLine(details.query)}".`));
|
|
88
89
|
return text;
|
|
89
90
|
}
|
|
90
91
|
|
|
@@ -92,19 +93,21 @@ export function registerSearchTool(pi: ExtensionAPI, deps: SearchDeps = {}): voi
|
|
|
92
93
|
// A blank line separates the call header from the result body.
|
|
93
94
|
const lines: string[] = [
|
|
94
95
|
"",
|
|
95
|
-
theme
|
|
96
|
+
paint(theme, "faint", `via ${details.provider}${SEPARATORS.item}${count} result${count === 1 ? "" : "s"}`),
|
|
96
97
|
];
|
|
97
|
-
if (details.answer)
|
|
98
|
+
if (details.answer)
|
|
99
|
+
lines.push(`${paint(theme, "faint", "answer")} ${paint(theme, "meta", oneLine(details.answer, 160))}`);
|
|
98
100
|
|
|
99
101
|
const shown = options.expanded ? details.results : details.results.slice(0, 5);
|
|
100
|
-
const indent = theme
|
|
102
|
+
const indent = paint(theme, "faint", " ");
|
|
101
103
|
for (const [index, item] of shown.entries()) {
|
|
102
|
-
lines.push(`${theme
|
|
103
|
-
lines.push(`${indent}${theme
|
|
104
|
-
if (options.expanded && item.snippet)
|
|
104
|
+
lines.push(`${paint(theme, "accent", `${index + 1}.`)} ${paint(theme, "meta", oneLine(item.title, 100))}`);
|
|
105
|
+
lines.push(`${indent}${paint(theme, "faint", oneLine(item.url, 100))}`);
|
|
106
|
+
if (options.expanded && item.snippet)
|
|
107
|
+
lines.push(`${indent}${paint(theme, "meta", oneLine(item.snippet, 160))}`);
|
|
105
108
|
}
|
|
106
109
|
const extra = count - shown.length;
|
|
107
|
-
if (extra > 0) lines.push(`${theme
|
|
110
|
+
if (extra > 0) lines.push(`${paint(theme, "faint", `+${extra} more`)} ${expandHint(theme)}`);
|
|
108
111
|
text.setText(lines.join("\n"));
|
|
109
112
|
return text;
|
|
110
113
|
},
|
|
@@ -252,8 +252,8 @@ status, which is what the old start/stop guidance used to do.
|
|
|
252
252
|
| `outbound.ts` | Upload and send a local file: hash, encrypt, `getUploadUrl`, upload, `sendFileMessage`. |
|
|
253
253
|
| `login.ts` | QR login state machine. |
|
|
254
254
|
| `qrcode.ts` | QR matrix → Unicode half-block terminal lines. |
|
|
255
|
-
| `login-screen.ts` | The `/wechat login` screen (`ctx.ui.custom`). |
|
|
256
|
-
| `status-screen.ts` | The `/wechat` control screen (`ctx.ui.custom`) and the pure one-line/body renderers. |
|
|
255
|
+
| `login-screen.ts` | The `/wechat login` screen (`ctx.ui.custom`), drawn from the `lib/tui/` kit (`titleRule`, `clip`, `isCloseKey`). |
|
|
256
|
+
| `status-screen.ts` | The `/wechat` control screen (`ctx.ui.custom`) and the pure one-line/body renderers, drawn from the `lib/tui/` kit (`titleRule`, `keyValueRow`, `paint`). |
|
|
257
257
|
| `config.ts` | The `wechat` section of `bytefly.json` loading and normalization. |
|
|
258
258
|
| `credentials.ts` | `credentials.json` (`0600`, multi-account). |
|
|
259
259
|
| `state.ts` | `state.json` (cursor, owner, per-peer tokens). |
|
|
@@ -14,7 +14,8 @@
|
|
|
14
14
|
import type { ExtensionAPI, ExtensionContext, Theme } from "@earendil-works/pi-coding-agent";
|
|
15
15
|
import { join } from "node:path";
|
|
16
16
|
import { isExtensionEnabled } from "../../lib/env.ts";
|
|
17
|
-
import { GLYPHS, STATUS_KEYS } from "../../lib/
|
|
17
|
+
import { GLYPHS, STATUS_KEYS } from "../../lib/tui/glyphs.ts";
|
|
18
|
+
import { paint } from "../../lib/tui/tokens.ts";
|
|
18
19
|
import { createBridge, type Bridge, type BridgeState } from "./bridge.ts";
|
|
19
20
|
import { createFetchRunner, WechatClient } from "./client.ts";
|
|
20
21
|
import { registerCommands, type CommandDeps } from "./commands.ts";
|
|
@@ -34,9 +35,9 @@ import { registerTools } from "./tools.ts";
|
|
|
34
35
|
export function wechatChip(theme: Theme, state: BridgeState): string | undefined {
|
|
35
36
|
switch (state) {
|
|
36
37
|
case "running":
|
|
37
|
-
return theme
|
|
38
|
+
return paint(theme, "ok", `${GLYPHS.wechat} wechat`);
|
|
38
39
|
case "expired":
|
|
39
|
-
return theme
|
|
40
|
+
return paint(theme, "bad", `${GLYPHS.wechat} wechat expired`);
|
|
40
41
|
case "stopped":
|
|
41
42
|
return undefined;
|
|
42
43
|
}
|
|
@@ -7,8 +7,11 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
10
|
+
import { type Component, visibleWidth } from "@earendil-works/pi-tui";
|
|
11
|
+
import { screenHint } from "../../lib/tui/chrome.ts";
|
|
12
|
+
import { clip } from "../../lib/tui/columns.ts";
|
|
13
|
+
import { isCloseKey } from "../../lib/tui/cursor.ts";
|
|
14
|
+
import { titleRule } from "../../lib/tui/rules.ts";
|
|
12
15
|
|
|
13
16
|
export interface LoginViewState {
|
|
14
17
|
/** Raw `get_qrcode_status` value, or `wait` before the first poll. */
|
|
@@ -51,7 +54,7 @@ export function loginBody(state: LoginViewState, width: number): string[] {
|
|
|
51
54
|
lines.push(state.url.slice(index, index + target));
|
|
52
55
|
}
|
|
53
56
|
}
|
|
54
|
-
return lines.map((line) =>
|
|
57
|
+
return lines.map((line) => clip(line, target));
|
|
55
58
|
}
|
|
56
59
|
|
|
57
60
|
export interface LoginScreenOptions {
|
|
@@ -69,7 +72,7 @@ export class LoginScreenComponent implements Component {
|
|
|
69
72
|
constructor(private readonly options: LoginScreenOptions) {}
|
|
70
73
|
|
|
71
74
|
handleInput(data: string): void {
|
|
72
|
-
if (
|
|
75
|
+
if (isCloseKey(data)) {
|
|
73
76
|
this.options.onCancel();
|
|
74
77
|
}
|
|
75
78
|
}
|
|
@@ -81,7 +84,7 @@ export class LoginScreenComponent implements Component {
|
|
|
81
84
|
const target = Math.max(1, width);
|
|
82
85
|
const theme = this.options.theme;
|
|
83
86
|
return [
|
|
84
|
-
|
|
87
|
+
titleRule(theme, target, "wechat · login"),
|
|
85
88
|
"",
|
|
86
89
|
...loginBody(this.options.state(), target),
|
|
87
90
|
"",
|
|
@@ -10,8 +10,13 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
13
|
+
import type { Component } from "@earendil-works/pi-tui";
|
|
14
|
+
import { screenHint, type ScreenHint } from "../../lib/tui/chrome.ts";
|
|
15
|
+
import { clip } from "../../lib/tui/columns.ts";
|
|
16
|
+
import { isCloseKey } from "../../lib/tui/cursor.ts";
|
|
17
|
+
import { keyValueRow } from "../../lib/tui/rows.ts";
|
|
18
|
+
import { titleRule } from "../../lib/tui/rules.ts";
|
|
19
|
+
import { paint, type TuiRole } from "../../lib/tui/tokens.ts";
|
|
15
20
|
import type { BridgeStatus } from "./bridge.ts";
|
|
16
21
|
import { peerLabel } from "./format.ts";
|
|
17
22
|
import type { BotCredentials } from "./types.ts";
|
|
@@ -72,18 +77,32 @@ export function statusLine(view: StatusView): string {
|
|
|
72
77
|
/** Label column width in the screen body. */
|
|
73
78
|
const LABEL_WIDTH = 11;
|
|
74
79
|
|
|
75
|
-
|
|
76
|
-
|
|
80
|
+
/** One body row: a `meta` label column and a value in `valueRole`. */
|
|
81
|
+
function row(theme: Theme, width: number, label: string, value: string, valueRole: TuiRole = "label"): string {
|
|
82
|
+
return keyValueRow(theme, width, { label, value, labelWidth: LABEL_WIDTH, labelRole: "meta", valueRole });
|
|
77
83
|
}
|
|
78
84
|
|
|
79
|
-
|
|
85
|
+
/** The connection word, uncolored; {@link row} paints it. */
|
|
86
|
+
function connectionText(connection: StatusConnection): string {
|
|
80
87
|
switch (connection) {
|
|
81
88
|
case "open":
|
|
82
|
-
return
|
|
89
|
+
return "open";
|
|
83
90
|
case "expired":
|
|
84
|
-
return
|
|
91
|
+
return "expired";
|
|
85
92
|
case "closed":
|
|
86
|
-
return
|
|
93
|
+
return "closed";
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** The design role the connection word is drawn in. */
|
|
98
|
+
function connectionRole(connection: StatusConnection): TuiRole {
|
|
99
|
+
switch (connection) {
|
|
100
|
+
case "open":
|
|
101
|
+
return "ok";
|
|
102
|
+
case "expired":
|
|
103
|
+
return "bad";
|
|
104
|
+
case "closed":
|
|
105
|
+
return "meta";
|
|
87
106
|
}
|
|
88
107
|
}
|
|
89
108
|
|
|
@@ -91,28 +110,28 @@ function connectionText(theme: Theme, connection: StatusConnection): string {
|
|
|
91
110
|
export function statusBody(view: StatusView, theme: Theme, width: number): string[] {
|
|
92
111
|
const target = Math.max(1, width);
|
|
93
112
|
const lines = [
|
|
94
|
-
row(theme, "Account", view.account ??
|
|
95
|
-
row(theme, "Connection", connectionText(
|
|
113
|
+
row(theme, target, "Account", view.account ?? "not logged in", view.account ? "label" : "meta"),
|
|
114
|
+
row(theme, target, "Connection", connectionText(view.connection), connectionRole(view.connection)),
|
|
96
115
|
];
|
|
97
116
|
if (view.bridge) {
|
|
98
|
-
if (view.owner && view.owner !== view.account) lines.push(row(theme, "Owner", view.owner));
|
|
99
|
-
lines.push(row(theme, "Peers", String(view.peers)));
|
|
100
|
-
lines.push(row(theme, "Queued", String(view.queued)));
|
|
101
|
-
if (view.refused > 0) lines.push(row(theme, "Refused", String(view.refused)));
|
|
102
|
-
if (view.mediaDropped > 0) lines.push(row(theme, "Media", `${view.mediaDropped} dropped
|
|
103
|
-
if (view.promptPending) lines.push(row(theme,
|
|
117
|
+
if (view.owner && view.owner !== view.account) lines.push(row(theme, target, "Owner", view.owner));
|
|
118
|
+
lines.push(row(theme, target, "Peers", String(view.peers), "meta"));
|
|
119
|
+
lines.push(row(theme, target, "Queued", String(view.queued), "meta"));
|
|
120
|
+
if (view.refused > 0) lines.push(row(theme, target, "Refused", String(view.refused), "meta"));
|
|
121
|
+
if (view.mediaDropped > 0) lines.push(row(theme, target, "Media", `${view.mediaDropped} dropped`, "meta"));
|
|
122
|
+
if (view.promptPending) lines.push(row(theme, target, "Prompt", "waiting for a reply", "accent"));
|
|
104
123
|
}
|
|
105
124
|
if (view.connection === "expired") {
|
|
106
125
|
lines.push("");
|
|
107
|
-
lines.push(` ${theme
|
|
126
|
+
lines.push(clip(` ${paint(theme, "bad", "Session expired — press l to log in again.")}`, target));
|
|
108
127
|
} else if (view.connection === "closed" && view.account) {
|
|
109
128
|
lines.push("");
|
|
110
|
-
lines.push(` ${theme
|
|
129
|
+
lines.push(clip(` ${paint(theme, "meta", "Not connected — press l to log in again.")}`, target));
|
|
111
130
|
} else if (view.connection === "closed") {
|
|
112
131
|
lines.push("");
|
|
113
|
-
lines.push(` ${theme
|
|
132
|
+
lines.push(clip(` ${paint(theme, "meta", "Run /wechat login to connect an account.")}`, target));
|
|
114
133
|
}
|
|
115
|
-
return lines
|
|
134
|
+
return lines;
|
|
116
135
|
}
|
|
117
136
|
|
|
118
137
|
export interface StatusScreenOptions {
|
|
@@ -140,7 +159,7 @@ export class StatusScreenComponent implements Component {
|
|
|
140
159
|
}
|
|
141
160
|
|
|
142
161
|
handleInput(data: string): void {
|
|
143
|
-
if (
|
|
162
|
+
if (isCloseKey(data)) {
|
|
144
163
|
this.options.onClose("close");
|
|
145
164
|
return;
|
|
146
165
|
}
|
|
@@ -166,7 +185,7 @@ export class StatusScreenComponent implements Component {
|
|
|
166
185
|
const target = Math.max(1, width);
|
|
167
186
|
const theme = this.options.theme;
|
|
168
187
|
return [
|
|
169
|
-
|
|
188
|
+
titleRule(theme, target, "wechat · control"),
|
|
170
189
|
"",
|
|
171
190
|
...statusBody(this.options.view(), theme, target),
|
|
172
191
|
"",
|
|
@@ -11,7 +11,8 @@ import type { JsonValue } from "@earendil-works/pi-ai";
|
|
|
11
11
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
12
12
|
import { Type, type Static } from "typebox";
|
|
13
13
|
import { sanitizeLine } from "../../lib/format.ts";
|
|
14
|
-
import { reuseText, toolDetails } from "../../lib/tui.ts";
|
|
14
|
+
import { reuseText, toolDetails } from "../../lib/tui/chrome.ts";
|
|
15
|
+
import { paint } from "../../lib/tui/tokens.ts";
|
|
15
16
|
import type { Bridge } from "./bridge.ts";
|
|
16
17
|
|
|
17
18
|
export const TOOL_NAME = "send_wechat";
|
|
@@ -85,7 +86,7 @@ export function registerTools(pi: ExtensionAPI, deps: ToolDeps): void {
|
|
|
85
86
|
const text = reuseText(context.lastComponent);
|
|
86
87
|
const body = (args as { text?: unknown }).text;
|
|
87
88
|
const label = context.argsComplete && typeof body === "string" ? sanitizeLine(body) : "…";
|
|
88
|
-
text.setText(theme.fg("toolTitle", theme.bold(`${TOOL_NAME} `)) + theme
|
|
89
|
+
text.setText(theme.fg("toolTitle", theme.bold(`${TOOL_NAME} `)) + paint(theme, "meta", label));
|
|
89
90
|
return text;
|
|
90
91
|
},
|
|
91
92
|
|
|
@@ -93,16 +94,16 @@ export function registerTools(pi: ExtensionAPI, deps: ToolDeps): void {
|
|
|
93
94
|
const text = reuseText(context.lastComponent);
|
|
94
95
|
const details = toolDetails<{ sent?: boolean; error?: string }>(result.details);
|
|
95
96
|
if (details?.sent) {
|
|
96
|
-
text.setText(theme
|
|
97
|
+
text.setText(paint(theme, "ok", "sent to WeChat owner"));
|
|
97
98
|
return text;
|
|
98
99
|
}
|
|
99
100
|
if (details?.error) {
|
|
100
|
-
text.setText(theme
|
|
101
|
+
text.setText(paint(theme, "bad", `Error: ${sanitizeLine(details.error)}`));
|
|
101
102
|
return text;
|
|
102
103
|
}
|
|
103
104
|
// Pi's rejection result carries `{}`; show its message instead.
|
|
104
105
|
const first = result.content[0];
|
|
105
|
-
text.setText(first?.type === "text" ? first.text : theme
|
|
106
|
+
text.setText(first?.type === "text" ? first.text : paint(theme, "bad", "Error: send failed"));
|
|
106
107
|
return text;
|
|
107
108
|
},
|
|
108
109
|
});
|
|
@@ -7,7 +7,7 @@ import { appendFileSync, existsSync, readFileSync } from "node:fs";
|
|
|
7
7
|
import { basename, join, resolve } from "node:path";
|
|
8
8
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
9
9
|
import { isInside } from "../../lib/path.ts";
|
|
10
|
-
import { GLYPHS } from "../../lib/
|
|
10
|
+
import { GLYPHS } from "../../lib/tui/glyphs.ts";
|
|
11
11
|
import { loadConfig, type WorktreeConfig } from "./config.ts";
|
|
12
12
|
import { WorktreeCancelledError } from "./errors.ts";
|
|
13
13
|
import {
|
|
@@ -24,7 +24,7 @@ import { existsSync } from "node:fs";
|
|
|
24
24
|
import { fileURLToPath } from "node:url";
|
|
25
25
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
26
26
|
import { isExtensionEnabled } from "../../lib/env.ts";
|
|
27
|
-
import { GLYPHS } from "../../lib/
|
|
27
|
+
import { GLYPHS } from "../../lib/tui/glyphs.ts";
|
|
28
28
|
import { canonicalize, checkCheckout, repoRoot, unlockWorktree } from "./git.ts";
|
|
29
29
|
import { analyzeBashCommand, guardFileTool } from "./guard.ts";
|
|
30
30
|
import {
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
11
|
-
import { STATUS_KEYS } from "../../lib/
|
|
11
|
+
import { STATUS_KEYS } from "../../lib/tui/glyphs.ts";
|
|
12
12
|
import { ENV_ROOT } from "../../lib/env.ts";
|
|
13
13
|
import { loadConfig, type WorktreeConfig } from "./config.ts";
|
|
14
14
|
import type { WorktreeState } from "./state.ts";
|
package/lib/README.md
CHANGED
|
@@ -24,7 +24,7 @@ belongs in that extension instead — see `extensions/web-access/http.ts` and
|
|
|
24
24
|
| [`interaction.ts`](./interaction.ts) | Turn-affinity prompt contract: the channel a remote adapter installs on `ctx.ui` under a string key, the `InteractionChannel` interface (`request` / `post` / `postFile`), and `remoteChannel` / `isRemoteTurn` / `supportsCustomUI` / `askHuman` (with an optional file/text preface) plus the shared “Other” label. |
|
|
25
25
|
| [`git/`](./git/) | Shared git plumbing. [`git/runner.ts`](./git/runner.ts) is the `RunGit` seam (`createExecRunner` with timeout/env handling, `runGitOrThrow`, `GitError`); [`git/read.ts`](./git/read.ts) holds the read helpers (`repoRoot`, `repoRootFor`, `gitDir`, `revParse`, `hasCommits`, `currentBranch`); [`git/index.ts`](./git/index.ts) is the barrel. |
|
|
26
26
|
| [`highlight.ts`](./highlight.ts) | First-party, escape-first syntax tokenizer (`detectLanguage`, `fenceLanguage`, `highlightLines`, `HighlightLanguage`, `TokenClass`) for js/ts, json, css, html, shell, python, java, yaml, and sql; shared by `file-browse` and plan's code blocks. |
|
|
27
|
-
| [`
|
|
27
|
+
| [`tui/`](./tui/) | The Bytefly terminal design system shared by every interactive surface: [`tui/tokens.ts`](./tui/tokens.ts) (semantic role -> theme color, `paint`), [`tui/glyphs.ts`](./tui/glyphs.ts) (`GLYPHS`, `SEPARATORS`, `STATUS_KEYS`, `EXPAND_KEYBINDING` with `expandKey` / `expandHint`, `BODY_INDENT` / `GLYPH_GAP` — the vocabulary both sides of a status must agree on), [`tui/chrome.ts`](./tui/chrome.ts) (`screenHint`, `dockListFrame` (the docked-list frame), `viewportRows`, `FULL_SCREEN_OVERLAY`, `reuseText`, `editorTheme`), [`tui/rules.ts`](./tui/rules.ts) (`plainRule` / `titleRule` / `centeredRule`), [`tui/rows.ts`](./tui/rows.ts) (`listRow`, `checkbox`, `chip`, `keyValueRow`, `emptyState`), [`tui/columns.ts`](./tui/columns.ts) (`splitRow`, `fitCells`, `flowCells`, `padTo`, `clip`), [`tui/gauge.ts`](./tui/gauge.ts) (`computeGauge`, `gaugeRole`, `gaugeBar`), [`tui/line-pane.ts`](./tui/line-pane.ts) (`LinePane`, the scrollable log/match pane), [`tui/status-controller.ts`](./tui/status-controller.ts) (`createStatusController`, the footer chips `job` and `watch` share), and [`tui/cursor.ts`](./tui/cursor.ts) (scroll and navigation: `fitRows`, `keepVisible`, `clampScroll`, `navIntent`, `navigateList`, `navigateScroll`, `wheelDelta`, `scrollList`, `isCloseKey`, `selectionMarker`, `formatRange`, and the `ListCursor` (by index) and `IdCursor` (by stable id) state). |
|
|
28
28
|
| [`path.ts`](./path.ts) | `isInside` (string containment) plus `realPathOfNearest` / `isInsideReal` (symlink-aware): the containment predicates the file-browse, worktree, and doc guards share. |
|
|
29
29
|
| [`pi-invocation.ts`](./pi-invocation.ts) | `getPiInvocation`: how to launch a nested `pi` process, shared by `subagent` and bytefly's doctor surface. |
|
|
30
30
|
| [`policy.ts`](./policy.ts) | `createReadOnlyPolicy`: default-deny tool classification from `readOnlyHint` + explicit allow/deny. |
|
|
@@ -32,8 +32,6 @@ belongs in that extension instead — see `extensions/web-access/http.ts` and
|
|
|
32
32
|
| [`requirements.ts`](./requirements.ts) | Per-extension runtime prerequisite table plus a pure `probeExtension(seams)` over injected seams (exec, config reads, optional imports): bytefly's doctor surface reports on it and the extensions that own a dependency format their errors from it. |
|
|
33
33
|
| [`shell.ts`](./shell.ts) | Shell escape rule shared by the worktree isolation guard and plan mode's read-only git guard: `SHELL_ESCAPABLE`, `isShellEscapable`, `hasShellEscape`. |
|
|
34
34
|
| [`tool-names.ts`](./tool-names.ts) | Tool names and one parameter name that more than one extension must agree on, so an orchestrating `ctx.executeTool()` call or schema probe breaks the build on a rename. |
|
|
35
|
-
| [`tui.ts`](./tui.ts) | Screen chrome: `screenHeader`, `screenHint`, `dockListFrame` (the shared docked-list assembly), `viewportRows`, and `FULL_SCREEN_OVERLAY`, plus `reuseText` (per-render `Text` reuse) and `editorTheme` (inline-editor theme). |
|
|
36
|
-
| [`ui.ts`](./ui.ts) | Shared UI vocabulary: `GLYPHS`, `SEPARATORS`, `STATUS_KEYS` (`ctx.ui.setStatus` keys), `EXPAND_KEYBINDING` with `expandKey` / `expandHint`, and the transcript rail `BODY_INDENT` / `GLYPH_GAP`. |
|
|
37
35
|
| [`web/`](./web/) | The Bytefly web brand system shared by the browser surfaces: [`web/tokens.ts`](./web/tokens.ts) (palette derived from the Bytefly terminal themes, scales, and the `tok-*` map), [`web/base.ts`](./web/base.ts), [`web/components.ts`](./web/components.ts), [`web/layout.ts`](./web/layout.ts), [`web/runtime.ts`](./web/runtime.ts) (`WEB_THEME_STORAGE_KEY`, `THEME_JS`, `THEME_TOGGLE_JS`), [`web/markup.ts`](./web/markup.ts), and the [`web/index.ts`](./web/index.ts) barrel. |
|
|
38
36
|
|
|
39
37
|
## Conventions
|
|
@@ -43,7 +41,12 @@ belongs in that extension instead — see `extensions/web-access/http.ts` and
|
|
|
43
41
|
- **Inject the host seam.** Modules that need a host capability take a function
|
|
44
42
|
(`RunGit`, `HttpRunner`) rather than the whole `ExtensionAPI`, so the logic is
|
|
45
43
|
testable without a runtime.
|
|
46
|
-
- **Constants that must agree across both sides live in `
|
|
47
|
-
the `setStatus` key are one contract, not two literals. Tool names
|
|
48
|
-
an orchestrating tool and the tool it calls — and parameter names
|
|
49
|
-
extension probes on another — live in `tool-names.ts`.
|
|
44
|
+
- **Constants that must agree across both sides live in `tui/glyphs.ts`** — a
|
|
45
|
+
glyph and the `setStatus` key are one contract, not two literals. Tool names
|
|
46
|
+
shared by an orchestrating tool and the tool it calls — and parameter names
|
|
47
|
+
one extension probes on another — live in `tool-names.ts`.
|
|
48
|
+
- **A color is a role, not a token.** Draw through [`tui/tokens.ts`](./tui/tokens.ts)
|
|
49
|
+
(`paint(theme, role, text)`) so a theme swap re-skins every surface; the
|
|
50
|
+
primitives in `tui/` already do, and a new one should too. The one exception is
|
|
51
|
+
Pi's tool-rendering vocabulary (`toolOutput`, `toolTitle`), which stays direct:
|
|
52
|
+
those tokens style a tool block, and no role maps to them.
|
package/lib/requirements.ts
CHANGED
|
@@ -166,7 +166,7 @@ export const REQUIREMENTS: Readonly<Record<string, ExtensionRequirements>> = {
|
|
|
166
166
|
optional: [{ kind: "module", name: "unpdf", enables: "PDF text extraction in `web_fetch`", hint: pdfGuidance() }],
|
|
167
167
|
},
|
|
168
168
|
"file-browse": {
|
|
169
|
-
optional: [gitCheck("the branch chip")],
|
|
169
|
+
optional: [gitCheck("the branch chip and the diff view")],
|
|
170
170
|
notes: ["The default-browser opener (`open` / `xdg-open` / `cmd`) is best-effort; the URL is printed either way."],
|
|
171
171
|
},
|
|
172
172
|
document: {},
|
|
@@ -194,7 +194,7 @@ export const REQUIREMENTS: Readonly<Record<string, ExtensionRequirements>> = {
|
|
|
194
194
|
bytefly: {
|
|
195
195
|
configFiles: [SHARED_CONFIG_FILE],
|
|
196
196
|
notes: [
|
|
197
|
-
"TUI only for the header, footer, and turn rule; `/bytefly`
|
|
197
|
+
"TUI only for the header, footer, and turn rule; `/bytefly` prints a listing and `/bytefly doctor` the report in RPC, JSON, and print modes.",
|
|
198
198
|
"The panel writes extension enablement through Pi's own settings; project scope requires a trusted project.",
|
|
199
199
|
"`/bytefly`'s list screen and the proactive missing-prerequisite warning need an interactive UI.",
|
|
200
200
|
],
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
|
|
19
19
|
import type { Theme, ThemeColor } from "@earendil-works/pi-coding-agent";
|
|
20
20
|
import { Text, truncateToWidth, visibleWidth, type EditorTheme, type OverlayOptions } from "@earendil-works/pi-tui";
|
|
21
|
-
import { SEPARATORS } from "./
|
|
21
|
+
import { SEPARATORS } from "./glyphs.ts";
|
|
22
22
|
|
|
23
23
|
/**
|
|
24
24
|
* `ctx.ui.custom` options that give a screen the whole terminal.
|
|
@@ -36,21 +36,6 @@ export const FULL_SCREEN_OVERLAY: { overlay: true; overlayOptions: OverlayOption
|
|
|
36
36
|
overlayOptions: { width: "100%", maxHeight: "100%", anchor: "top-left", margin: 0 },
|
|
37
37
|
};
|
|
38
38
|
|
|
39
|
-
/**
|
|
40
|
-
* Top border with `label` centered-left, exactly `width` columns wide. When the
|
|
41
|
-
* terminal is too narrow for the title and a border on each side, it degrades to
|
|
42
|
-
* a plain rule rather than truncating the title into an ellipsis.
|
|
43
|
-
*/
|
|
44
|
-
export function screenHeader(theme: Theme, width: number, label: string): string {
|
|
45
|
-
const text = ` ${label} `;
|
|
46
|
-
const prefix = "───";
|
|
47
|
-
if (width < visibleWidth(prefix) + visibleWidth(text) + 1) {
|
|
48
|
-
return theme.fg("borderMuted", "─".repeat(Math.max(0, width)));
|
|
49
|
-
}
|
|
50
|
-
const remaining = width - visibleWidth(prefix) - visibleWidth(text);
|
|
51
|
-
return theme.fg("borderMuted", prefix) + theme.fg("accent", text) + theme.fg("borderMuted", "─".repeat(remaining));
|
|
52
|
-
}
|
|
53
|
-
|
|
54
39
|
/** A row count, or a live getter so a screen re-sizes with the terminal. */
|
|
55
40
|
export type ViewportRowsSource = number | (() => number | undefined);
|
|
56
41
|
|
|
@@ -105,12 +90,8 @@ export function screenHint(theme: Theme, width: number, hints: ScreenHint[]): st
|
|
|
105
90
|
* the frame only places them.
|
|
106
91
|
*/
|
|
107
92
|
export interface DockListFrame {
|
|
108
|
-
/**
|
|
109
|
-
|
|
110
|
-
/** Body width in columns. */
|
|
111
|
-
width: number;
|
|
112
|
-
/** The header label, e.g. `Extensions`. */
|
|
113
|
-
title: string;
|
|
93
|
+
/** The pre-rendered header line, usually `titleRule(theme, width, title)`. */
|
|
94
|
+
header: string;
|
|
114
95
|
/** A summary line under the header; a blank line follows it. */
|
|
115
96
|
summary?: string;
|
|
116
97
|
/** The list rows, or the screen's single empty-state line. */
|
|
@@ -124,18 +105,16 @@ export interface DockListFrame {
|
|
|
124
105
|
}
|
|
125
106
|
|
|
126
107
|
/**
|
|
127
|
-
* Assemble a docked list screen: the
|
|
128
|
-
* its blank line, the body, an optional range row, optional detail rows, and
|
|
108
|
+
* Assemble a docked list screen: the caller's header rule, an optional summary
|
|
109
|
+
* and its blank line, the body, an optional range row, optional detail rows, and
|
|
129
110
|
* the blank-line-padded key-hint footer.
|
|
130
111
|
*
|
|
131
|
-
* This is the convention
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
* so it stays hand-rolled rather than changing its output to fit this helper.
|
|
112
|
+
* This is the convention every docked list screen shares — the bytefly panel,
|
|
113
|
+
* `/jobs`, `/watch`, `/todos`, `/memory`, and `/rewind`. The header line is built
|
|
114
|
+
* by the caller (usually `titleRule`), so the frame never picks a rule style.
|
|
135
115
|
*/
|
|
136
116
|
export function dockListFrame(frame: DockListFrame): string[] {
|
|
137
|
-
const
|
|
138
|
-
const lines: string[] = [screenHeader(frame.theme, width, frame.title)];
|
|
117
|
+
const lines: string[] = [frame.header];
|
|
139
118
|
if (frame.summary !== undefined) lines.push(frame.summary, "");
|
|
140
119
|
lines.push(...frame.body);
|
|
141
120
|
if (frame.range !== undefined) lines.push(frame.range);
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Width arithmetic for the terminal: justify, fit, pad, and clip lines.
|
|
3
|
+
*
|
|
4
|
+
* Every function works in terminal columns via `visibleWidth` /
|
|
5
|
+
* `truncateToWidth`, never string length, and every result fits the width it
|
|
6
|
+
* was given. Themed strings pass through unchanged except where a cell has to
|
|
7
|
+
* be clipped, so a color survives every cell that fits.
|
|
8
|
+
*
|
|
9
|
+
* Value-only: pure functions over their arguments.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
13
|
+
|
|
14
|
+
/** A usable origin: terminal arithmetic starts from one column, never zero. */
|
|
15
|
+
function columns(width: number): number {
|
|
16
|
+
return Math.max(1, Math.floor(width));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Clip a possibly-themed string to `width` columns, marking a cut with `ellipsis`. */
|
|
20
|
+
export function clip(text: string, width: number, ellipsis = "…"): string {
|
|
21
|
+
return truncateToWidth(text, columns(width), ellipsis);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Pad `text` on the right to exactly `width` columns. Never truncates. */
|
|
25
|
+
export function padTo(text: string, width: number): string {
|
|
26
|
+
const missing = columns(width) - visibleWidth(text);
|
|
27
|
+
return missing > 0 ? text + " ".repeat(missing) : text;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Left/right justify on one line: the left cell sits at the margin, the right
|
|
32
|
+
* cell at the edge, and at least `minGap` columns separate them.
|
|
33
|
+
*
|
|
34
|
+
* The right cell is the anchor — it is clipped only after the left cell has
|
|
35
|
+
* been clipped away — so a value at the end of a line (a model name, a metric)
|
|
36
|
+
* survives a tight terminal instead of vanishing. An empty right cell means a
|
|
37
|
+
* plain left-aligned line with no trailing spaces.
|
|
38
|
+
*/
|
|
39
|
+
export function splitRow(left: string, right: string, width: number, minGap = 1): string {
|
|
40
|
+
const w = columns(width);
|
|
41
|
+
if (!right) return clip(left, w);
|
|
42
|
+
const gap = Math.max(0, Math.floor(minGap));
|
|
43
|
+
const rightCell = clip(right, w);
|
|
44
|
+
const rightWidth = visibleWidth(rightCell);
|
|
45
|
+
const leftBudget = w - rightWidth - gap;
|
|
46
|
+
if (leftBudget <= 0) return rightCell;
|
|
47
|
+
return padTo(clip(left, leftBudget), w - rightWidth) + rightCell;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Join as many whole cells as fit, dropping trailing cells rather than
|
|
52
|
+
* clipping one in half. The first cell is always kept, so an over-wide line
|
|
53
|
+
* degrades to a clipped first cell instead of an empty one.
|
|
54
|
+
*/
|
|
55
|
+
export function fitCells(cells: readonly string[], width: number, separator = " "): string {
|
|
56
|
+
const w = columns(width);
|
|
57
|
+
const kept: string[] = [];
|
|
58
|
+
for (const cell of cells) {
|
|
59
|
+
const candidate = [...kept, cell].join(separator);
|
|
60
|
+
if (kept.length > 0 && visibleWidth(candidate) > w) break;
|
|
61
|
+
kept.push(cell);
|
|
62
|
+
}
|
|
63
|
+
return clip(kept.join(separator), w);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Lay atomic, already-themed cells onto lines no wider than `width`.
|
|
68
|
+
*
|
|
69
|
+
* Cells are never split: one that would not fit moves to the next line, and one
|
|
70
|
+
* wider than the whole width is clipped. Use this for a row of independent
|
|
71
|
+
* tokens (tabs, tags) where `fitCells`' single-line drop would lose one; unlike
|
|
72
|
+
* {@link fitCells} it may return several lines, and always returns one.
|
|
73
|
+
*/
|
|
74
|
+
export function flowCells(cells: readonly string[], width: number): string[] {
|
|
75
|
+
const w = columns(width);
|
|
76
|
+
const lines: string[] = [];
|
|
77
|
+
let current = "";
|
|
78
|
+
let currentWidth = 0;
|
|
79
|
+
for (const cell of cells) {
|
|
80
|
+
let text = cell;
|
|
81
|
+
let cellWidth = visibleWidth(cell);
|
|
82
|
+
if (cellWidth > w) {
|
|
83
|
+
text = truncateToWidth(cell, w);
|
|
84
|
+
cellWidth = visibleWidth(text);
|
|
85
|
+
}
|
|
86
|
+
if (currentWidth > 0 && currentWidth + cellWidth > w) {
|
|
87
|
+
lines.push(current);
|
|
88
|
+
current = "";
|
|
89
|
+
currentWidth = 0;
|
|
90
|
+
}
|
|
91
|
+
current += text;
|
|
92
|
+
currentWidth += cellWidth;
|
|
93
|
+
}
|
|
94
|
+
lines.push(current);
|
|
95
|
+
return lines;
|
|
96
|
+
}
|