@gavin-hu/my-pi-agent 0.18.0 → 0.20.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 +66 -52
- package/README.zh-CN.md +56 -46
- package/docs/extensions/README.md +34 -0
- package/docs/extensions/board.md +507 -0
- package/{extensions/bytefly/README.md → docs/extensions/bytefly.md} +774 -574
- package/{extensions/check/README.md → docs/extensions/check.md} +83 -68
- package/{extensions/context/README.md → docs/extensions/context.md} +40 -23
- package/docs/extensions/file-browse.md +561 -0
- package/{extensions/file-open/README.md → docs/extensions/file-open.md} +31 -23
- package/{extensions/file-read/README.md → docs/extensions/file-read.md} +47 -55
- package/{extensions/goal/README.md → docs/extensions/goal.md} +115 -131
- package/{extensions/job/README.md → docs/extensions/job.md} +62 -52
- package/{extensions/memory/README.md → docs/extensions/memory.md} +71 -60
- package/{extensions/plan/README.md → docs/extensions/plan.md} +440 -322
- package/{extensions/question/README.md → docs/extensions/question.md} +54 -31
- package/{extensions/rewind/README.md → docs/extensions/rewind.md} +115 -122
- package/docs/extensions/session-bus.md +298 -0
- package/{extensions/subagent/README.md → docs/extensions/subagent.md} +165 -118
- package/docs/extensions/team.md +254 -0
- package/{extensions/todo/README.md → docs/extensions/todo.md} +85 -88
- package/{extensions/watch/README.md → docs/extensions/watch.md} +54 -36
- package/{extensions/web-access/README.md → docs/extensions/web-access.md} +93 -50
- package/{extensions/wechat/README.md → docs/extensions/wechat.md} +260 -163
- package/{extensions/worktree/README.md → docs/extensions/worktree.md} +137 -79
- package/extensions/board/commands.ts +105 -0
- package/extensions/board/config.ts +36 -0
- package/extensions/board/format.ts +170 -0
- package/extensions/board/index.ts +40 -0
- package/extensions/board/input.ts +54 -0
- package/extensions/board/join.ts +42 -0
- package/extensions/board/page.ts +388 -0
- package/extensions/board/result.ts +108 -0
- package/extensions/board/rows.ts +48 -0
- package/extensions/board/schema.ts +90 -0
- package/extensions/board/seat.ts +28 -0
- package/extensions/board/snapshot.ts +42 -0
- package/extensions/board/tools.ts +444 -0
- package/extensions/board/transport/router.ts +91 -0
- package/extensions/board/transport/server.ts +138 -0
- package/extensions/board/types.ts +121 -0
- package/extensions/board/view/client.ts +51 -0
- package/extensions/board/view/dialog.ts +43 -0
- package/extensions/board/view/keys.ts +125 -0
- package/extensions/board/view/live.ts +231 -0
- package/extensions/board/view/names.ts +91 -0
- package/extensions/board/view/page.ts +118 -0
- package/extensions/board/view/render.ts +519 -0
- package/extensions/board/view/styles.ts +228 -0
- package/extensions/board/view/team.ts +327 -0
- package/extensions/bytefly/footer/budget.ts +6 -5
- package/extensions/bytefly/footer/collect/entries.ts +12 -4
- package/extensions/bytefly/footer/collect/snapshot.ts +8 -17
- package/extensions/bytefly/footer/config.ts +19 -8
- package/extensions/bytefly/footer/price/price.ts +30 -8
- package/extensions/bytefly/footer/register.ts +5 -5
- package/extensions/bytefly/footer/render/format.ts +18 -0
- package/extensions/bytefly/footer/render/lines.ts +23 -7
- package/extensions/bytefly/footer/render/segments.ts +117 -31
- package/extensions/bytefly/footer/types.ts +29 -5
- package/extensions/bytefly/footer/unpriced.ts +42 -17
- package/extensions/bytefly/panel/commands.ts +67 -7
- package/extensions/bytefly/panel/filter.ts +68 -0
- package/extensions/bytefly/panel/format.ts +16 -15
- package/extensions/bytefly/panel/join.ts +56 -13
- package/extensions/bytefly/panel/prices.ts +43 -0
- package/extensions/bytefly/panel/settings-runtime.ts +0 -6
- package/extensions/bytefly/panel/settings.ts +75 -43
- package/extensions/bytefly/panel/tui/entries.ts +26 -8
- package/extensions/bytefly/panel/tui/frame.ts +130 -93
- package/extensions/bytefly/panel/tui/keys.ts +352 -0
- package/extensions/bytefly/panel/tui/list-view.ts +25 -46
- package/extensions/bytefly/panel/tui/panes.ts +76 -0
- package/extensions/bytefly/panel/tui/section-view.ts +126 -0
- package/extensions/bytefly/panel/tui/settings-section.ts +54 -125
- package/extensions/bytefly/panel/tui/settings-view.ts +30 -131
- package/extensions/bytefly/panel/tui/side-pane.ts +224 -0
- package/extensions/bytefly/panel/tui.ts +306 -259
- package/extensions/bytefly/picker/discovery.ts +0 -2
- package/extensions/bytefly/picker/types.ts +0 -2
- package/extensions/bytefly/update/check.ts +39 -0
- package/extensions/bytefly/update/format.ts +15 -0
- package/extensions/bytefly/update/register.ts +73 -34
- package/extensions/bytefly/update/state.ts +6 -3
- package/extensions/check/commands.ts +2 -1
- package/extensions/context/tui.ts +1 -1
- package/extensions/file-browse/{config.ts → config/config.ts} +1 -1
- package/extensions/file-browse/files/classify.ts +136 -0
- package/extensions/file-browse/files/files.ts +12 -0
- package/extensions/file-browse/files/index.ts +90 -0
- package/extensions/file-browse/files/list.ts +261 -0
- package/extensions/file-browse/{paths.ts → files/paths.ts} +1 -1
- package/extensions/file-browse/files/tree.ts +101 -0
- package/extensions/file-browse/{git.ts → git/git.ts} +10 -3
- package/extensions/file-browse/index.ts +4 -4
- package/extensions/file-browse/{live.ts → live/live.ts} +20 -69
- package/extensions/file-browse/{router.ts → transport/router.ts} +21 -17
- package/extensions/file-browse/transport/server.ts +110 -0
- package/extensions/file-browse/{chrome.ts → view/chrome.ts} +4 -4
- package/extensions/file-browse/{client.ts → view/client.ts} +1 -1
- package/extensions/file-browse/{diff-html.ts → view/diff-html.ts} +3 -3
- package/extensions/file-browse/view/emit.ts +51 -0
- package/extensions/file-browse/{html.ts → view/html.ts} +19 -18
- package/extensions/file-browse/{icons.ts → view/icons.ts} +1 -1
- package/extensions/file-browse/{markdown.ts → view/markdown.ts} +18 -20
- package/extensions/file-browse/{raw-html.ts → view/raw-html.ts} +19 -14
- package/extensions/file-browse/{styles.ts → view/styles.ts} +29 -5
- package/extensions/file-browse/{tree.ts → view/tree.ts} +1 -1
- package/extensions/file-browse/{urls.ts → view/urls.ts} +1 -1
- package/extensions/file-read/schema.ts +2 -1
- package/extensions/job/runtime.ts +4 -4
- package/extensions/memory/store.ts +12 -5
- package/extensions/plan/blocks.ts +23 -2
- package/extensions/plan/commands.ts +21 -16
- package/extensions/plan/feedback.ts +185 -52
- package/extensions/plan/format.ts +10 -3
- package/extensions/plan/index.ts +23 -5
- package/extensions/plan/plan-files.ts +88 -0
- package/extensions/plan/plan-index.ts +43 -9
- package/extensions/plan/plan-text.ts +33 -23
- package/extensions/plan/plans.ts +27 -34
- package/extensions/plan/policy.ts +30 -17
- package/extensions/plan/review.ts +3 -27
- package/extensions/plan/runtime.ts +52 -11
- package/extensions/plan/sections.ts +112 -0
- package/extensions/plan/steps.ts +3 -22
- package/extensions/plan/todos.ts +51 -3
- package/extensions/plan/tools/exit.ts +20 -95
- package/extensions/plan/tools/todo-seed.ts +4 -2
- package/extensions/plan/tools/write.ts +12 -1
- package/extensions/plan/tui.ts +5 -34
- package/extensions/plan/types.ts +15 -8
- package/extensions/plan/web-blocks.ts +89 -0
- package/extensions/plan/web-chrome.ts +3 -5
- package/extensions/plan/web-client.ts +179 -330
- package/extensions/plan/web-collab.ts +3 -3
- package/extensions/plan/web-comments.ts +18 -8
- package/extensions/plan/web-composer.ts +80 -0
- package/extensions/plan/web-draft.ts +40 -0
- package/extensions/plan/web-keys.ts +134 -0
- package/extensions/plan/web-outline.ts +192 -0
- package/extensions/plan/web-page.ts +4 -8
- package/extensions/plan/web-plan.ts +150 -0
- package/extensions/plan/web-router.ts +3 -58
- package/extensions/plan/web-settle.ts +194 -0
- package/extensions/plan/web-share-session.ts +24 -30
- package/extensions/plan/web-streams.ts +14 -14
- package/extensions/plan/web-tokens.ts +2 -2
- package/extensions/plan/web.ts +66 -120
- package/extensions/rewind/{commands.ts → commands/commands.ts} +5 -5
- package/extensions/rewind/{review.ts → commands/review.ts} +15 -14
- package/extensions/rewind/{rewind.ts → commands/rewind.ts} +6 -6
- package/extensions/rewind/{config.ts → config/config.ts} +1 -1
- package/extensions/rewind/index.ts +3 -3
- package/extensions/rewind/{policy.ts → state/policy.ts} +4 -4
- package/extensions/rewind/{runtime.ts → state/runtime.ts} +11 -11
- package/extensions/rewind/{snapshot-run.ts → state/snapshot-run.ts} +1 -1
- package/extensions/rewind/{timeline.ts → state/timeline.ts} +2 -2
- package/extensions/rewind/{format.ts → text/format.ts} +3 -3
- package/extensions/rewind/{git.ts → transport/git.ts} +2 -2
- package/extensions/rewind/{restore.ts → transport/restore.ts} +2 -2
- package/extensions/rewind/{snapshot.ts → transport/snapshot.ts} +1 -1
- package/extensions/rewind/{store.ts → transport/store.ts} +1 -1
- package/extensions/rewind/{checklist.ts → view/checklist.ts} +6 -6
- package/extensions/rewind/{hunk-picker.ts → view/hunk-picker.ts} +8 -8
- package/extensions/rewind/{path-picker.ts → view/path-picker.ts} +7 -7
- package/extensions/rewind/{review-tui.ts → view/review-tui.ts} +11 -11
- package/extensions/rewind/{rewind-tui.ts → view/rewind-tui.ts} +17 -11
- package/extensions/rewind/{status.ts → view/status.ts} +4 -4
- package/extensions/session-bus/commands.ts +17 -69
- package/extensions/session-bus/format.ts +22 -45
- package/extensions/session-bus/index.ts +13 -7
- package/extensions/session-bus/join-request.ts +40 -0
- package/extensions/session-bus/mail-watch.ts +134 -0
- package/extensions/session-bus/notice.ts +32 -4
- package/{lib/claims → extensions/session-bus}/overlap.ts +11 -18
- package/extensions/session-bus/protocol.ts +19 -49
- package/extensions/session-bus/resolve.ts +67 -0
- package/extensions/session-bus/result.ts +16 -51
- package/extensions/session-bus/rows.ts +11 -5
- package/extensions/session-bus/runtime.ts +220 -267
- package/extensions/session-bus/store.ts +55 -7
- package/extensions/session-bus/tools.ts +31 -66
- package/extensions/session-bus/tui.ts +5 -5
- package/extensions/session-bus/types.ts +23 -14
- package/extensions/subagent/agents.ts +9 -8
- package/extensions/subagent/commands.ts +2 -1
- package/extensions/subagent/invocation.ts +4 -2
- package/extensions/team/commands.ts +145 -0
- package/extensions/team/dispatch.ts +133 -0
- package/extensions/team/format.ts +119 -0
- package/extensions/team/index.ts +34 -0
- package/extensions/team/schema.ts +29 -0
- package/extensions/team/tools.ts +267 -0
- package/extensions/team/types.ts +65 -0
- package/extensions/todo/index.ts +4 -1
- package/extensions/todo/nudge.ts +8 -2
- package/extensions/web-access/fetch/page.ts +4 -1
- package/extensions/web-access/fetch/structured.ts +447 -0
- package/extensions/web-access/fetch/tool.ts +2 -1
- package/extensions/web-access/search/tool.ts +2 -1
- package/extensions/wechat/bridge.ts +106 -251
- package/extensions/wechat/{commands.ts → commands/commands.ts} +26 -18
- package/extensions/wechat/{doctor.ts → commands/doctor.ts} +5 -5
- package/extensions/wechat/{history.ts → commands/history.ts} +1 -1
- package/extensions/wechat/{login.ts → commands/login.ts} +3 -3
- package/extensions/wechat/{slash.ts → commands/slash.ts} +38 -2
- package/extensions/wechat/dialog/channels.ts +136 -0
- package/extensions/wechat/{interaction.ts → dialog/interaction.ts} +37 -37
- package/extensions/wechat/{prompt.ts → dialog/prompt.ts} +2 -2
- package/extensions/wechat/{remote-ui.ts → dialog/remote-ui.ts} +1 -1
- package/extensions/wechat/index.ts +10 -10
- package/extensions/wechat/intake.ts +245 -0
- package/extensions/wechat/{admission.ts → peer/admission.ts} +2 -2
- package/extensions/wechat/{peer-session.ts → peer/peer-session.ts} +2 -2
- package/extensions/wechat/{peer-worker.ts → peer/peer-worker.ts} +100 -22
- package/extensions/wechat/{presence.ts → peer/presence.ts} +8 -7
- package/extensions/wechat/{reply-pipeline.ts → peer/reply-pipeline.ts} +1 -1
- package/extensions/wechat/{credentials.ts → state/credentials.ts} +2 -2
- package/extensions/wechat/{state.ts → state/state.ts} +14 -2
- package/extensions/wechat/{format.ts → text/format.ts} +3 -3
- package/extensions/wechat/{notices.ts → text/notices.ts} +8 -0
- package/extensions/wechat/tools.ts +29 -12
- package/extensions/wechat/{client.ts → transport/client.ts} +26 -2
- package/extensions/wechat/{deliver.ts → transport/deliver.ts} +72 -28
- package/extensions/wechat/{inbound.ts → transport/inbound.ts} +5 -5
- package/extensions/wechat/{media.ts → transport/media.ts} +11 -5
- package/extensions/wechat/transport/outbound.ts +142 -0
- package/extensions/wechat/{poll.ts → transport/poll.ts} +3 -3
- package/extensions/wechat/types.ts +10 -1
- package/extensions/wechat/{login-screen.ts → view/login-screen.ts} +4 -4
- package/extensions/wechat/{status-screen.ts → view/status-screen.ts} +31 -26
- package/extensions/worktree/commands.ts +24 -17
- package/extensions/worktree/exit.ts +12 -6
- package/extensions/worktree/git.ts +22 -14
- package/extensions/worktree/guard.ts +18 -290
- package/extensions/worktree/handoff.ts +10 -2
- package/extensions/worktree/index.ts +2 -1
- package/extensions/worktree/lifecycle.ts +2 -0
- package/extensions/worktree/registry.ts +20 -11
- package/extensions/worktree/root-tools.ts +13 -3
- package/extensions/worktree/runtime.ts +8 -2
- package/extensions/worktree/screen.ts +15 -8
- package/extensions/worktree/shell.ts +303 -0
- package/extensions/worktree/state.ts +1 -1
- package/extensions/worktree/status.ts +42 -9
- package/extensions/worktree/tools.ts +19 -13
- package/lib/README.md +11 -8
- package/lib/board/card.ts +479 -0
- package/lib/board/links.ts +141 -0
- package/lib/board/page-pointer.ts +310 -0
- package/lib/board/round-text.ts +81 -0
- package/lib/board/round.ts +94 -0
- package/lib/board/rules.ts +437 -0
- package/lib/board/session.ts +105 -0
- package/lib/board/store.ts +497 -0
- package/lib/board/view.ts +234 -0
- package/lib/errors.ts +12 -4
- package/lib/format.ts +35 -0
- package/lib/http/guard.ts +112 -0
- package/lib/http/index.ts +16 -5
- package/lib/http/serve.ts +237 -0
- package/lib/http/server.ts +12 -10
- package/lib/http/sink.ts +135 -0
- package/lib/http/token.ts +38 -0
- package/lib/plan-entry.ts +11 -0
- package/lib/policy.ts +25 -2
- package/lib/requirements.ts +6 -7
- package/lib/session-label.ts +64 -0
- package/lib/session-roster.ts +201 -0
- package/lib/session-tool.ts +184 -0
- package/lib/spawn.ts +37 -3
- package/lib/stray-fields.ts +45 -0
- package/lib/tool-names.ts +38 -2
- package/lib/tui/glyphs.ts +3 -2
- package/lib/web/README.md +57 -4
- package/lib/web/client-runtime.ts +123 -0
- package/lib/web/components.ts +5 -0
- package/lib/web/index.ts +4 -2
- package/lib/web/markup.ts +88 -1
- package/lib/web/names.ts +42 -0
- package/lib/web/runtime.ts +7 -1
- package/lib/web/tokens.ts +25 -2
- package/package.json +26 -14
- package/skills/next/SKILL.md +4 -3
- package/skills/project-docs/SKILL.md +133 -0
- package/skills/project-docs/references/audit.md +131 -0
- package/skills/project-docs/references/changelog.md +105 -0
- package/skills/project-docs/references/discovery.md +127 -0
- package/skills/project-docs/references/readme.md +80 -0
- package/skills/project-docs/references/release.md +84 -0
- package/skills/project-docs/references/roadmap.md +93 -0
- package/skills/project-docs/scripts/state.ts +316 -0
- package/skills/refactor/SKILL.md +100 -125
- package/skills/refactor/references/handoff.md +35 -0
- package/skills/refactor/references/signals.md +54 -0
- package/skills/refactor/scripts/inventory.ts +587 -0
- package/extensions/bytefly/panel/tui/extensions-view.ts +0 -40
- package/extensions/bytefly/panel/tui/skills-view.ts +0 -50
- package/extensions/file-browse/README.md +0 -506
- package/extensions/file-browse/files.ts +0 -564
- package/extensions/file-browse/server.ts +0 -141
- package/extensions/job/process.ts +0 -35
- package/extensions/plan/split.ts +0 -87
- package/extensions/session-bus/README.md +0 -247
- package/extensions/session-bus/team.ts +0 -102
- package/extensions/wechat/outbound.ts +0 -100
- package/lib/plans/sections.ts +0 -172
- package/lib/plans/setting.ts +0 -52
- package/lib/plans/split.ts +0 -215
- package/lib/plans/store.ts +0 -19
- package/lib/plans/team.ts +0 -151
- package/skills/changelog/SKILL.md +0 -148
- package/skills/parallel-dev/SKILL.md +0 -150
- /package/extensions/file-browse/{rev.ts → live/rev.ts} +0 -0
- /package/extensions/file-browse/{diff.ts → text/diff.ts} +0 -0
- /package/extensions/file-browse/{fuzzy.ts → text/fuzzy.ts} +0 -0
- /package/extensions/file-browse/{live-client.ts → view/live-client.ts} +0 -0
- /package/extensions/file-browse/{palette.ts → view/palette.ts} +0 -0
- /package/extensions/rewind/{patch.ts → text/patch.ts} +0 -0
- /package/extensions/rewind/{paths.ts → text/paths.ts} +0 -0
- /package/extensions/rewind/{queue.ts → transport/queue.ts} +0 -0
- /package/extensions/rewind/{temp-index.ts → transport/temp-index.ts} +0 -0
- /package/extensions/wechat/{lock.ts → state/lock.ts} +0 -0
- /package/extensions/wechat/{channel.ts → text/channel.ts} +0 -0
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* The catalogue is the shortlist a session changes, not the whole document: every
|
|
10
10
|
* row is one a workflow differs on, and each states its own rule so a value the
|
|
11
11
|
* screen accepts is a value the loader keeps. The catalogue order is the screen's
|
|
12
|
-
* order, and it is capped so the section fits a 24-row terminal (`
|
|
12
|
+
* order, and it is capped so the section fits a 24-row terminal (`settings.test.ts`
|
|
13
13
|
* holds the ceiling); a new row has to displace one. Everything else stays
|
|
14
14
|
* `bytefly.json` — a list (`doctor.skip`), a table (`price.rates`), a schedule
|
|
15
15
|
* (`price.peak`), and the tuning numbers and labels this screen deliberately does
|
|
@@ -25,13 +25,14 @@
|
|
|
25
25
|
*/
|
|
26
26
|
|
|
27
27
|
import { configSection, configValueAt } from "../../../lib/config.ts";
|
|
28
|
-
import { SPLIT_KEY, SPLIT_SECTION, SPLIT_SETTING_ID, SPLIT_SETTING_LABEL } from "../../../lib/plans/setting.ts";
|
|
29
28
|
import { DEFAULT_CONFIG as DEFAULT_DOCTOR_CONFIG, normalizeConfig as normalizeDoctorConfig } from "../doctor/config.ts";
|
|
30
29
|
import {
|
|
31
30
|
CURRENCY_DEFAULTS,
|
|
31
|
+
CURRENCY_SYMBOLS,
|
|
32
32
|
DEFAULT_BYTEFLY_CONFIG,
|
|
33
33
|
DEFAULT_CURRENCY,
|
|
34
34
|
MAX_BUDGET,
|
|
35
|
+
MAX_DECIMALS,
|
|
35
36
|
normalizeByteflyConfig,
|
|
36
37
|
} from "../footer/price/config.ts";
|
|
37
38
|
import type { CurrencyCode } from "../footer/price/types.ts";
|
|
@@ -39,7 +40,7 @@ import { DEFAULT_NOTIFY_CONFIG, normalizeNotifyConfig } from "../notify/config.t
|
|
|
39
40
|
import type { Settings, WriteScope } from "../picker/types.ts";
|
|
40
41
|
|
|
41
42
|
/** Where a preference is stored. */
|
|
42
|
-
type SettingSource = "app" | "bytefly"
|
|
43
|
+
type SettingSource = "app" | "bytefly";
|
|
43
44
|
|
|
44
45
|
/** The row control a preference uses. */
|
|
45
46
|
type SettingKind = "boolean" | "enum" | "text";
|
|
@@ -47,6 +48,13 @@ type SettingKind = "boolean" | "enum" | "text";
|
|
|
47
48
|
/** Where the effective value came from. */
|
|
48
49
|
type SettingOrigin = "default" | "global" | "project" | "inherited";
|
|
49
50
|
|
|
51
|
+
/**
|
|
52
|
+
* The heading a row sits under. The screen draws one heading per group, so twelve
|
|
53
|
+
* rows read as four short lists: what a session starts with, what it tells you
|
|
54
|
+
* about, what it keeps, and what it costs.
|
|
55
|
+
*/
|
|
56
|
+
export type SettingGroup = "Startup" | "Notifications" | "Session" | "Price";
|
|
57
|
+
|
|
50
58
|
/** One curated value a row can take. */
|
|
51
59
|
interface SettingOption {
|
|
52
60
|
value: string;
|
|
@@ -62,7 +70,7 @@ interface TextRule {
|
|
|
62
70
|
}
|
|
63
71
|
|
|
64
72
|
/** The `bytefly.json` sections a row still reaches. */
|
|
65
|
-
export type ConfigSectionName = "notify" | "doctor" | "price" | "rewind" | "job"
|
|
73
|
+
export type ConfigSectionName = "notify" | "doctor" | "price" | "rewind" | "job";
|
|
66
74
|
|
|
67
75
|
/** Where a `bytefly.json` row lives: a section, and the path to its leaf. */
|
|
68
76
|
interface SettingConfig {
|
|
@@ -81,6 +89,8 @@ interface SettingLeaf {
|
|
|
81
89
|
export interface SettingDescriptor {
|
|
82
90
|
/** Stable identity, also the focus key. */
|
|
83
91
|
id: string;
|
|
92
|
+
/** The heading this row is listed under. */
|
|
93
|
+
group: SettingGroup;
|
|
84
94
|
label: string;
|
|
85
95
|
description: string;
|
|
86
96
|
source: SettingSource;
|
|
@@ -125,8 +135,6 @@ const ON_OFF: readonly SettingOption[] = [
|
|
|
125
135
|
{ value: "true", label: "On" },
|
|
126
136
|
];
|
|
127
137
|
|
|
128
|
-
/** The notify section's delivery methods, labeled by the surface each reaches. */
|
|
129
|
-
|
|
130
138
|
/**
|
|
131
139
|
* A text rule for a money amount inside an inclusive range: a whole number or a
|
|
132
140
|
* decimal with up to four places, matching the loader's clamp for `price.budget`.
|
|
@@ -148,15 +156,17 @@ function amountRule(min: number, max: number): TextRule {
|
|
|
148
156
|
/**
|
|
149
157
|
* The catalogue, in screen order.
|
|
150
158
|
*
|
|
151
|
-
* Global scope lists all
|
|
159
|
+
* Global scope lists all twelve; Project lists the ten `bytefly.json` rows,
|
|
152
160
|
* because Pi's setters write the personal `settings.json` only. The list is
|
|
153
161
|
* capped so the screen keeps fitting a 24-row terminal.
|
|
154
162
|
*/
|
|
155
163
|
export const SETTING_ROWS: readonly SettingDescriptor[] = [
|
|
156
164
|
{
|
|
157
165
|
id: "byteflyUi",
|
|
158
|
-
|
|
159
|
-
|
|
166
|
+
group: "Startup",
|
|
167
|
+
label: "Startup screen",
|
|
168
|
+
description:
|
|
169
|
+
"Show bytefly's header in place of Pi's own, so a session opens on the versions, the help keys, and the update marker.",
|
|
160
170
|
source: "app",
|
|
161
171
|
kind: "boolean",
|
|
162
172
|
fallback: "false",
|
|
@@ -165,9 +175,10 @@ export const SETTING_ROWS: readonly SettingDescriptor[] = [
|
|
|
165
175
|
},
|
|
166
176
|
{
|
|
167
177
|
id: "theme",
|
|
178
|
+
group: "Startup",
|
|
168
179
|
label: "Theme",
|
|
169
180
|
description:
|
|
170
|
-
"
|
|
181
|
+
"The palette every bytefly surface is drawn with; `system` follows Pi's own colors. Takes effect after reload.",
|
|
171
182
|
source: "app",
|
|
172
183
|
kind: "enum",
|
|
173
184
|
fallback: "system",
|
|
@@ -176,8 +187,9 @@ export const SETTING_ROWS: readonly SettingDescriptor[] = [
|
|
|
176
187
|
},
|
|
177
188
|
{
|
|
178
189
|
id: "notify.enabled",
|
|
190
|
+
group: "Notifications",
|
|
179
191
|
label: "Desktop notifications",
|
|
180
|
-
description: "
|
|
192
|
+
description: "The master switch for OS notifications; with it off, none of the four below is raised.",
|
|
181
193
|
source: "bytefly",
|
|
182
194
|
kind: "boolean",
|
|
183
195
|
fallback: "false",
|
|
@@ -187,8 +199,9 @@ export const SETTING_ROWS: readonly SettingDescriptor[] = [
|
|
|
187
199
|
},
|
|
188
200
|
{
|
|
189
201
|
id: "notify.turnFinished",
|
|
190
|
-
|
|
191
|
-
|
|
202
|
+
group: "Notifications",
|
|
203
|
+
label: "When a turn ends",
|
|
204
|
+
description: "Raise a notification once the agent has finished a reply.",
|
|
192
205
|
source: "bytefly",
|
|
193
206
|
kind: "boolean",
|
|
194
207
|
fallback: "true",
|
|
@@ -198,8 +211,9 @@ export const SETTING_ROWS: readonly SettingDescriptor[] = [
|
|
|
198
211
|
},
|
|
199
212
|
{
|
|
200
213
|
id: "notify.needsInput",
|
|
201
|
-
|
|
202
|
-
|
|
214
|
+
group: "Notifications",
|
|
215
|
+
label: "When the agent needs you",
|
|
216
|
+
description: "Raise a notification when a question or a permission prompt is waiting.",
|
|
203
217
|
source: "bytefly",
|
|
204
218
|
kind: "boolean",
|
|
205
219
|
fallback: "true",
|
|
@@ -209,8 +223,9 @@ export const SETTING_ROWS: readonly SettingDescriptor[] = [
|
|
|
209
223
|
},
|
|
210
224
|
{
|
|
211
225
|
id: "notify.jobFinished",
|
|
212
|
-
|
|
213
|
-
|
|
226
|
+
group: "Notifications",
|
|
227
|
+
label: "When a job ends",
|
|
228
|
+
description: "Raise a notification when a background job or a watcher settles.",
|
|
214
229
|
source: "bytefly",
|
|
215
230
|
kind: "boolean",
|
|
216
231
|
fallback: "true",
|
|
@@ -220,8 +235,9 @@ export const SETTING_ROWS: readonly SettingDescriptor[] = [
|
|
|
220
235
|
},
|
|
221
236
|
{
|
|
222
237
|
id: "notify.matchFound",
|
|
223
|
-
|
|
224
|
-
|
|
238
|
+
group: "Notifications",
|
|
239
|
+
label: "When a watcher matches",
|
|
240
|
+
description: "Raise a notification when a watched pattern appears, once the agent is idle.",
|
|
225
241
|
source: "bytefly",
|
|
226
242
|
kind: "boolean",
|
|
227
243
|
fallback: "true",
|
|
@@ -231,8 +247,9 @@ export const SETTING_ROWS: readonly SettingDescriptor[] = [
|
|
|
231
247
|
},
|
|
232
248
|
{
|
|
233
249
|
id: "job.wakeOnFinish",
|
|
234
|
-
|
|
235
|
-
|
|
250
|
+
group: "Session",
|
|
251
|
+
label: "Resume after a job",
|
|
252
|
+
description: "Wake the agent once a background job finishes, so it can read the output.",
|
|
236
253
|
source: "bytefly",
|
|
237
254
|
kind: "boolean",
|
|
238
255
|
fallback: "false",
|
|
@@ -242,8 +259,9 @@ export const SETTING_ROWS: readonly SettingDescriptor[] = [
|
|
|
242
259
|
},
|
|
243
260
|
{
|
|
244
261
|
id: "rewind.autoSnapshots",
|
|
245
|
-
|
|
246
|
-
|
|
262
|
+
group: "Session",
|
|
263
|
+
label: "Snapshot before a turn",
|
|
264
|
+
description: "Keep one rewind point per prompt, so `/rewind` can take you back.",
|
|
247
265
|
source: "bytefly",
|
|
248
266
|
kind: "boolean",
|
|
249
267
|
fallback: "true",
|
|
@@ -253,8 +271,9 @@ export const SETTING_ROWS: readonly SettingDescriptor[] = [
|
|
|
253
271
|
},
|
|
254
272
|
{
|
|
255
273
|
id: "doctor.proactive",
|
|
256
|
-
|
|
257
|
-
|
|
274
|
+
group: "Session",
|
|
275
|
+
label: "Health check at startup",
|
|
276
|
+
description: "Probe what the extensions need before the first turn and say what is missing.",
|
|
258
277
|
source: "bytefly",
|
|
259
278
|
kind: "boolean",
|
|
260
279
|
fallback: "true",
|
|
@@ -264,9 +283,9 @@ export const SETTING_ROWS: readonly SettingDescriptor[] = [
|
|
|
264
283
|
},
|
|
265
284
|
{
|
|
266
285
|
id: "price.currency.code",
|
|
286
|
+
group: "Price",
|
|
267
287
|
label: "Currency",
|
|
268
|
-
description:
|
|
269
|
-
"CNY prices DeepSeek from its yuan rates; USD from its published dollars, falling back to Pi's recorded cost.",
|
|
288
|
+
description: "The currency the footer prices a session in, at that currency's published rates.",
|
|
270
289
|
source: "bytefly",
|
|
271
290
|
kind: "enum",
|
|
272
291
|
fallback: DEFAULT_CURRENCY.code,
|
|
@@ -286,8 +305,9 @@ export const SETTING_ROWS: readonly SettingDescriptor[] = [
|
|
|
286
305
|
},
|
|
287
306
|
{
|
|
288
307
|
id: "price.budget",
|
|
308
|
+
group: "Price",
|
|
289
309
|
label: "Session budget",
|
|
290
|
-
description: "
|
|
310
|
+
description: "Where the footer's `spent/limit` warns: one notice and one notification at the limit; 0 is off.",
|
|
291
311
|
source: "bytefly",
|
|
292
312
|
kind: "text",
|
|
293
313
|
fallback: "0",
|
|
@@ -296,20 +316,6 @@ export const SETTING_ROWS: readonly SettingDescriptor[] = [
|
|
|
296
316
|
text: amountRule(0, MAX_BUDGET),
|
|
297
317
|
config: { section: "price", path: ["budget"] },
|
|
298
318
|
},
|
|
299
|
-
{
|
|
300
|
-
// The split's switch, with the label, id, and default taken from the module
|
|
301
|
-
// that reads it, so this row cannot describe a key nothing honours.
|
|
302
|
-
id: SPLIT_SETTING_ID,
|
|
303
|
-
label: SPLIT_SETTING_LABEL,
|
|
304
|
-
description:
|
|
305
|
-
"Let an approved plan carry a team of slices, which separate sessions join by name. Off keeps every plan one flow.",
|
|
306
|
-
source: "plan",
|
|
307
|
-
kind: "boolean",
|
|
308
|
-
fallback: "false",
|
|
309
|
-
json: "boolean",
|
|
310
|
-
options: ON_OFF,
|
|
311
|
-
config: { section: SPLIT_SECTION, path: [SPLIT_KEY] },
|
|
312
|
-
},
|
|
313
319
|
];
|
|
314
320
|
|
|
315
321
|
/** The one refusal for a Project write while the project is untrusted. */
|
|
@@ -346,6 +352,31 @@ function themeOptions(themes: readonly string[]): readonly SettingOption[] {
|
|
|
346
352
|
return names.map((name) => ({ value: name, label: name }));
|
|
347
353
|
}
|
|
348
354
|
|
|
355
|
+
/** The currency and precision a session prices in, through the price reader. */
|
|
356
|
+
function currencyOf(snapshot: SettingsSnapshot, scope: WriteScope): { code: CurrencyCode; decimals: number } {
|
|
357
|
+
const reader = SECTION_READERS.price;
|
|
358
|
+
const globalRaw = configSection(snapshot.global, "price");
|
|
359
|
+
const projectRaw = scope === "project" ? configSection(snapshot.project, "price") : undefined;
|
|
360
|
+
const code = reader?.(globalRaw, projectRaw, ["currency", "code"]);
|
|
361
|
+
const decimals = reader?.(globalRaw, projectRaw, ["currency", "decimals"]);
|
|
362
|
+
return {
|
|
363
|
+
code: typeof code === "string" && code in CURRENCY_SYMBOLS ? (code as CurrencyCode) : DEFAULT_CURRENCY.code,
|
|
364
|
+
decimals: typeof decimals === "number" ? Math.min(decimals, MAX_DECIMALS) : DEFAULT_CURRENCY.decimals,
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* The budget's display value: `Off` for zero, otherwise the amount in the currency
|
|
370
|
+
* the session prices in. A reader thinks in `¥5`, not in `5`, and in `Off` rather
|
|
371
|
+
* than in `0`.
|
|
372
|
+
*/
|
|
373
|
+
function budgetLabel(raw: string, snapshot: SettingsSnapshot, scope: WriteScope): string {
|
|
374
|
+
const amount = Number(raw);
|
|
375
|
+
if (!Number.isFinite(amount) || amount <= 0) return "Off";
|
|
376
|
+
const { code, decimals } = currencyOf(snapshot, scope);
|
|
377
|
+
return `${CURRENCY_SYMBOLS[code]}${amount.toFixed(decimals)}`;
|
|
378
|
+
}
|
|
379
|
+
|
|
349
380
|
/** The display label for `raw`, or the raw value when it is a valid custom one. */
|
|
350
381
|
function labelFor(row: SettingDescriptor, raw: string): string {
|
|
351
382
|
const option = row.options.find((entry) => entry.value === raw);
|
|
@@ -445,7 +476,8 @@ export function settingViews(snapshot: SettingsSnapshot, scope: WriteScope): Set
|
|
|
445
476
|
const options = row.id === "theme" ? themeOptions(snapshot.themes) : row.options;
|
|
446
477
|
const { value, origin } = row.source === "app" ? appValue(snapshot, row) : byteflyValue(snapshot, row, scope);
|
|
447
478
|
const view = { ...row, options, value };
|
|
448
|
-
|
|
479
|
+
const valueLabel = row.id === "price.budget" ? budgetLabel(value, snapshot, scope) : labelFor(view, value);
|
|
480
|
+
return { ...view, valueLabel, origin };
|
|
449
481
|
});
|
|
450
482
|
}
|
|
451
483
|
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The panel's entry model: one flat list of
|
|
2
|
+
* The panel's entry model: one flat list of display entries per section, made of
|
|
3
|
+
* group headings interleaved with the selectable rows under them.
|
|
3
4
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* Every section groups. A resource list groups by source — where an extension came
|
|
6
|
+
* from is something its row cannot say — and Settings groups by what the
|
|
7
|
+
* preference does, so twelve rows read as four short lists instead of one long
|
|
8
|
+
* one. Both are built from a model or a set of views, so the component only has to
|
|
9
|
+
* keep one cursor per section.
|
|
8
10
|
*
|
|
9
11
|
* Pure: the list is rebuilt from a model or a set of views, and navigation is
|
|
10
12
|
* index arithmetic.
|
|
@@ -76,9 +78,25 @@ export function entriesOf(model: PanelModel): PanelEntry[] {
|
|
|
76
78
|
}
|
|
77
79
|
|
|
78
80
|
/**
|
|
79
|
-
* The Settings entries: one
|
|
80
|
-
*
|
|
81
|
+
* The Settings entries: one heading per group, in catalogue order, then its rows.
|
|
82
|
+
* The group key names the heading so a filter cannot collide a setting id with it.
|
|
81
83
|
*/
|
|
82
84
|
export function settingEntries(views: readonly SettingView[]): PanelEntry[] {
|
|
83
|
-
|
|
85
|
+
const entries: PanelEntry[] = [];
|
|
86
|
+
for (const [group, settings] of groupSettings(views)) {
|
|
87
|
+
entries.push({ kind: "group", key: `setting:${group}`, label: group, count: settings.length });
|
|
88
|
+
for (const setting of settings) entries.push({ kind: "setting", setting });
|
|
89
|
+
}
|
|
90
|
+
return entries;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** The views by their group, in catalogue order of first appearance. */
|
|
94
|
+
function groupSettings(views: readonly SettingView[]): Map<string, SettingView[]> {
|
|
95
|
+
const groups = new Map<string, SettingView[]>();
|
|
96
|
+
for (const view of views) {
|
|
97
|
+
const list = groups.get(view.group) ?? [];
|
|
98
|
+
list.push(view);
|
|
99
|
+
groups.set(view.group, list);
|
|
100
|
+
}
|
|
101
|
+
return groups;
|
|
84
102
|
}
|
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The chrome around the panel's body: the
|
|
3
|
-
*
|
|
4
|
-
* the
|
|
2
|
+
* The chrome around the panel's body: the header rule with the screen's
|
|
3
|
+
* attention, the section tabs with their counts and the write scope, a group
|
|
4
|
+
* heading, the hint row that a transient notice replaces, and the frame all three
|
|
5
|
+
* sections share.
|
|
5
6
|
*
|
|
6
|
-
* Every section draws its body through
|
|
7
|
-
* spacing, roles, or the
|
|
7
|
+
* Every section draws its body through {@link panelFrame}, so the three cannot
|
|
8
|
+
* drift on spacing, roles, width arithmetic, or height: the body is padded to one
|
|
9
|
+
* target measured across all three sections, which is what keeps the dock from
|
|
10
|
+
* resizing when the tabs change.
|
|
8
11
|
*/
|
|
9
12
|
|
|
10
13
|
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
14
|
+
import { visibleWidth } from "@earendil-works/pi-tui";
|
|
11
15
|
import { sanitizeLine } from "../../../../lib/format.ts";
|
|
12
16
|
import {
|
|
13
17
|
detailBlockRows,
|
|
@@ -17,51 +21,86 @@ import {
|
|
|
17
21
|
type ViewportRowsSource,
|
|
18
22
|
} from "../../../../lib/tui/chrome.ts";
|
|
19
23
|
import { clip, fitCells, splitRow } from "../../../../lib/tui/columns.ts";
|
|
20
|
-
import {
|
|
24
|
+
import { formatRange, type ListCursor } from "../../../../lib/tui/cursor.ts";
|
|
25
|
+
import { SEPARATORS } from "../../../../lib/tui/glyphs.ts";
|
|
21
26
|
import { BODY_INDENT } from "../../../../lib/tui/indent.ts";
|
|
22
27
|
import { emptyState } from "../../../../lib/tui/rows.ts";
|
|
28
|
+
import { titleRule } from "../../../../lib/tui/rules.ts";
|
|
23
29
|
import { paint } from "../../../../lib/tui/tokens.ts";
|
|
24
|
-
import {
|
|
30
|
+
import { sectionLabel, type SummarySegment } from "../format.ts";
|
|
25
31
|
import type { PanelSection } from "../types.ts";
|
|
26
32
|
import { selectableRange, type PanelEntry } from "./entries.ts";
|
|
33
|
+
import { composeRows } from "./panes.ts";
|
|
27
34
|
|
|
28
35
|
/** Body rows when the terminal height is unknown. */
|
|
29
36
|
export const SCREEN_DEFAULT_ROWS = 16;
|
|
30
|
-
/** Fixed rows around the body: header,
|
|
37
|
+
/** Fixed rows around the body: header, tabs, blank, blank, hint, closing rule. */
|
|
31
38
|
export const FRAME_ROWS = 6;
|
|
32
|
-
/** Detail rows reserved for the focused row
|
|
39
|
+
/** Detail rows reserved for the focused row in the single-column layout. */
|
|
33
40
|
export const DETAIL_ROWS = 3;
|
|
34
|
-
/** Body rows that must survive before the
|
|
41
|
+
/** Body rows that must survive before the bottom pane collapses to nothing. */
|
|
35
42
|
export const MIN_BODY_ROWS = 3;
|
|
36
43
|
/** Detail rows when the terminal height is unknown and the focused row is expanded. */
|
|
37
44
|
export const EXPANDED_DETAIL_FALLBACK = 8;
|
|
38
|
-
/** Fixed body offset in the frame: header,
|
|
45
|
+
/** Fixed body offset in the frame: header, tabs, blank line. */
|
|
39
46
|
export const BODY_START = 3;
|
|
40
47
|
|
|
41
|
-
/** The body indent, shared by the note and
|
|
48
|
+
/** The body indent, shared by the note and any focused-row block. */
|
|
42
49
|
export const INDENT = " ".repeat(BODY_INDENT);
|
|
43
50
|
|
|
44
|
-
/**
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
+
/**
|
|
52
|
+
* The header rule: the screen's name first, the attention it found at the right
|
|
53
|
+
* edge. The right side is dropped whole on a narrow terminal rather than clipped
|
|
54
|
+
* into half a word; the tabs below carry the same facts.
|
|
55
|
+
*/
|
|
56
|
+
export function headerLine(theme: Theme, width: number, title: string, right: readonly SummarySegment[]): string {
|
|
57
|
+
const w = Math.max(1, width);
|
|
58
|
+
const separator = paint(theme, "faint", SEPARATORS.item);
|
|
59
|
+
const text = right.map((segment) => paint(theme, segment.color, segment.text)).join(separator);
|
|
60
|
+
const rightWidth = visibleWidth(text);
|
|
61
|
+
if (text === "" || rightWidth + title.length + 4 > w) return titleRule(theme, w, title);
|
|
62
|
+
return `${titleRule(theme, w - rightWidth - 1, title)} ${text}`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* The tab row: every section with how many rows it holds, then any filter chips,
|
|
67
|
+
* and the scope a change is written to at the right edge. The active tab takes the
|
|
68
|
+
* accent; the counts stay faint so the names read first.
|
|
69
|
+
*/
|
|
70
|
+
export function tabsLine(
|
|
71
|
+
theme: Theme,
|
|
72
|
+
width: number,
|
|
73
|
+
active: PanelSection,
|
|
74
|
+
counts: Record<PanelSection, number>,
|
|
75
|
+
options: { chips?: readonly SummarySegment[]; scope?: string } = {},
|
|
76
|
+
): string {
|
|
77
|
+
const separator = paint(theme, "faint", SEPARATORS.item);
|
|
78
|
+
const tabs = (["extensions", "skills", "settings"] as const).map(
|
|
79
|
+
(section) =>
|
|
80
|
+
paint(theme, section === active ? "accent" : "faint", sectionLabel(section)) +
|
|
81
|
+
paint(theme, "faint", ` ${counts[section]}`),
|
|
82
|
+
);
|
|
83
|
+
const chips = options.chips ?? [];
|
|
84
|
+
const left = [
|
|
85
|
+
` ${tabs.join(separator)}`,
|
|
86
|
+
...(chips.length > 0 ? [` ${chips.map((chip) => paint(theme, chip.color, chip.text)).join(separator)}`] : []),
|
|
87
|
+
].join("");
|
|
88
|
+
const right = options.scope === undefined ? "" : paint(theme, "meta", `writes ${options.scope}`);
|
|
89
|
+
return clip(splitRow(left, right, width), width);
|
|
51
90
|
}
|
|
52
91
|
|
|
53
92
|
/** One section's counts as whole segments, so a narrow screen drops a count. */
|
|
54
93
|
export function countsLine(theme: Theme, width: number, segments: readonly SummarySegment[]): string {
|
|
55
|
-
const separator = paint(theme, "faint",
|
|
94
|
+
const separator = paint(theme, "faint", SEPARATORS.item);
|
|
56
95
|
const styled = segments.map((segment) => paint(theme, segment.color, segment.text));
|
|
57
96
|
return fitCells(styled, Math.max(1, width - 2), separator);
|
|
58
97
|
}
|
|
59
98
|
|
|
60
|
-
/** One group heading: its label and how many rows sit under it. */
|
|
99
|
+
/** One group heading: its label, and how many rows sit under it. */
|
|
61
100
|
export function groupLine(theme: Theme, width: number, entry: { label: string; count: number }): string {
|
|
62
101
|
const label = paint(theme, "meta", sanitizeLine(entry.label));
|
|
63
|
-
const count = paint(theme, "faint", `(${entry.count})`);
|
|
64
|
-
return clip(` ${label}
|
|
102
|
+
const count = entry.count > 0 ? paint(theme, "faint", ` (${entry.count})`) : "";
|
|
103
|
+
return clip(` ${label}${count}`, width);
|
|
65
104
|
}
|
|
66
105
|
|
|
67
106
|
/** The key-hint row, or a section's notice that replaces it. */
|
|
@@ -74,7 +113,7 @@ export function noticeLine(theme: Theme, width: number, notice: string): string
|
|
|
74
113
|
return clip(`${INDENT}${paint(theme, "meta", sanitizeLine(notice))}`, width);
|
|
75
114
|
}
|
|
76
115
|
|
|
77
|
-
/** The saved-then-reload marker, shared by
|
|
116
|
+
/** The saved-then-reload marker, shared by every focused-row block. */
|
|
78
117
|
export function savedLine(theme: Theme, width: number, dirty: boolean): string[] {
|
|
79
118
|
if (!dirty) return [];
|
|
80
119
|
return [clip(`${INDENT}${paint(theme, "warn", "Saved · reload to apply")}`, width)];
|
|
@@ -87,50 +126,18 @@ export function totalRows(source: ViewportRowsSource | undefined): number | unde
|
|
|
87
126
|
}
|
|
88
127
|
|
|
89
128
|
/**
|
|
90
|
-
*
|
|
91
|
-
* expanded. Zero when there is no free row for a pane at all,
|
|
92
|
-
* terminal keeps its body
|
|
129
|
+
* The rows the bottom pane gets in the single-column layout: the fixed pane, or
|
|
130
|
+
* the free height when expanded. Zero when there is no free row for a pane at all,
|
|
131
|
+
* so a short terminal keeps its body.
|
|
93
132
|
*/
|
|
94
133
|
export function detailRows(expanded: boolean, viewportRows: ViewportRowsSource | undefined): number {
|
|
95
134
|
const total = totalRows(viewportRows);
|
|
96
135
|
if (total === undefined) return expanded ? EXPANDED_DETAIL_FALLBACK : DETAIL_ROWS;
|
|
97
|
-
// One row pays for the blank line the frame puts above the pane.
|
|
98
136
|
const available = total - FRAME_ROWS - MIN_BODY_ROWS - 1;
|
|
99
137
|
if (available < 1) return 0;
|
|
100
138
|
return expanded ? available : Math.min(DETAIL_ROWS, available);
|
|
101
139
|
}
|
|
102
140
|
|
|
103
|
-
/**
|
|
104
|
-
* Drop optional rows until the frame fits the viewport: the range row first,
|
|
105
|
-
* then the project note, then the blank above the detail pane, then the pane.
|
|
106
|
-
* The header, summary, one body row, and the hint always survive; a terminal
|
|
107
|
-
* shorter than those is left to the host.
|
|
108
|
-
*/
|
|
109
|
-
export function fitOptionalLines(options: {
|
|
110
|
-
viewportRows: ViewportRowsSource | undefined;
|
|
111
|
-
body: string[];
|
|
112
|
-
note: string | undefined;
|
|
113
|
-
range: string | undefined;
|
|
114
|
-
detail: string[];
|
|
115
|
-
}): { body: string[]; range: string | undefined; detail: string[]; noteShown: boolean; detailBlank: boolean } {
|
|
116
|
-
const total = totalRows(options.viewportRows);
|
|
117
|
-
let { body, range, detail } = options;
|
|
118
|
-
let noteShown = options.note !== undefined && body[0] === options.note;
|
|
119
|
-
if (total === undefined) return { body, range, detail, noteShown, detailBlank: true };
|
|
120
|
-
const fits = (blank: boolean): boolean =>
|
|
121
|
-
FRAME_ROWS + body.length + (range === undefined ? 0 : 1) + detailBlockRows(detail, blank) <= total;
|
|
122
|
-
if (!fits(true)) range = undefined;
|
|
123
|
-
if (!fits(true) && noteShown) {
|
|
124
|
-
body = body.slice(1);
|
|
125
|
-
noteShown = false;
|
|
126
|
-
}
|
|
127
|
-
// The separator goes before the pane does: the focused row's findings are
|
|
128
|
-
// worth more than the blank above them.
|
|
129
|
-
const detailBlank = fits(true) || !fits(false);
|
|
130
|
-
if (!fits(false)) detail = [];
|
|
131
|
-
return { body, range, detail, noteShown, detailBlank };
|
|
132
|
-
}
|
|
133
|
-
|
|
134
141
|
/** What {@link panelFrame} draws: the window, the body, and the chrome around it. */
|
|
135
142
|
export interface PanelFrameInput {
|
|
136
143
|
theme: Theme;
|
|
@@ -138,59 +145,89 @@ export interface PanelFrameInput {
|
|
|
138
145
|
cursor: ListCursor;
|
|
139
146
|
width: number;
|
|
140
147
|
header: string;
|
|
141
|
-
|
|
148
|
+
tabs: string;
|
|
142
149
|
entries: readonly PanelEntry[];
|
|
143
150
|
note?: string;
|
|
151
|
+
/** The focused row's block, drawn under the list below the pane breakpoint. */
|
|
144
152
|
detail: string[];
|
|
145
153
|
hint: string;
|
|
146
154
|
/** The line a section shows in place of its body when it has no entries. */
|
|
147
155
|
emptyLabel?: string;
|
|
156
|
+
/**
|
|
157
|
+
* The tallest section's row count. The body is padded to it (bounded by the
|
|
158
|
+
* viewport), so every section draws the same height.
|
|
159
|
+
*/
|
|
160
|
+
tallest: number;
|
|
161
|
+
/** The side pane's rows, in the wide layout only. */
|
|
162
|
+
side?: (rows: number) => string[];
|
|
148
163
|
draw: (entry: PanelEntry, index: number) => string | undefined;
|
|
149
164
|
}
|
|
150
165
|
|
|
166
|
+
/** The frame both sections share, and the state the caller has to remember. */
|
|
167
|
+
export interface PanelFrameResult {
|
|
168
|
+
lines: string[];
|
|
169
|
+
/** Whether the project note survived the row budget, for the click mapping. */
|
|
170
|
+
noteShown: boolean;
|
|
171
|
+
/** Body rows drawn, so a caller can map a click to a row. */
|
|
172
|
+
bodyRows: number;
|
|
173
|
+
}
|
|
174
|
+
|
|
151
175
|
/**
|
|
152
|
-
* The frame
|
|
153
|
-
*
|
|
154
|
-
* chrome around them.
|
|
155
|
-
*
|
|
176
|
+
* The frame every section draws: the cursor window, the padded body, the range
|
|
177
|
+
* row when the list scrolls, the optional rows that give way on a short terminal,
|
|
178
|
+
* and the chrome around them.
|
|
179
|
+
*
|
|
180
|
+
* The height comes from one place. With a side pane the body is the whole free
|
|
181
|
+
* height, capped by the tallest section; without one the bottom pane is paid for
|
|
182
|
+
* first and the body gets what is left. Either way `target` is the same for all
|
|
183
|
+
* three sections at a given terminal size.
|
|
156
184
|
*/
|
|
157
|
-
export function panelFrame(input: PanelFrameInput):
|
|
158
|
-
const { theme, viewportRows, cursor, width, entries, note,
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
185
|
+
export function panelFrame(input: PanelFrameInput): PanelFrameResult {
|
|
186
|
+
const { theme, viewportRows, cursor, width, entries, note, hint } = input;
|
|
187
|
+
const total = totalRows(viewportRows);
|
|
188
|
+
const wide = input.side !== undefined;
|
|
189
|
+
|
|
190
|
+
// Costs that a short terminal can shed, in the order they go: the bottom pane
|
|
191
|
+
// first (its facts move nowhere, but the list matters more), then the note.
|
|
192
|
+
let detail = wide ? [] : input.detail;
|
|
193
|
+
let noteShown = note !== undefined;
|
|
194
|
+
const budgetFor = (): number => {
|
|
195
|
+
if (total === undefined) return Math.max(MIN_BODY_ROWS, input.tallest);
|
|
196
|
+
return total - FRAME_ROWS - detailBlockRows(wide ? [] : detail) - (noteShown ? 1 : 0);
|
|
197
|
+
};
|
|
198
|
+
if (budgetFor() < MIN_BODY_ROWS && !wide) detail = [];
|
|
199
|
+
if (budgetFor() < MIN_BODY_ROWS && noteShown) noteShown = false;
|
|
200
|
+
|
|
201
|
+
const target = Math.max(1, Math.min(budgetFor(), Math.max(1, input.tallest)));
|
|
202
|
+
const capacity = Math.max(1, target - (noteShown && note !== undefined ? 1 : 0));
|
|
203
|
+
const scrolling = entries.length > capacity;
|
|
204
|
+
cursor.visible = Math.max(1, capacity - (scrolling ? 1 : 0));
|
|
165
205
|
cursor.sync(entries.length);
|
|
166
206
|
const { start, end } = cursor.window(entries.length);
|
|
167
207
|
|
|
168
208
|
const body: string[] = [];
|
|
169
|
-
if (note) body.push(note);
|
|
209
|
+
if (note !== undefined && noteShown) body.push(note);
|
|
170
210
|
if (entries.length === 0 && input.emptyLabel !== undefined) {
|
|
171
211
|
body.push(...emptyState(theme, width, { label: input.emptyLabel }));
|
|
172
212
|
}
|
|
173
213
|
for (let index = start; index < end; index += 1) {
|
|
174
|
-
const line = draw(entries[index], index);
|
|
214
|
+
const line = input.draw(entries[index], index);
|
|
175
215
|
if (line !== undefined) body.push(line);
|
|
176
216
|
}
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
}),
|
|
194
|
-
noteShown: fitted.noteShown,
|
|
195
|
-
};
|
|
217
|
+
if (scrolling) {
|
|
218
|
+
body.push(formatRange(theme, width, selectableRange(entries, { start, end })));
|
|
219
|
+
}
|
|
220
|
+
while (body.length < target) body.push("");
|
|
221
|
+
|
|
222
|
+
const side = input.side === undefined ? undefined : input.side(target);
|
|
223
|
+
const lines = dockListFrame({
|
|
224
|
+
theme,
|
|
225
|
+
width,
|
|
226
|
+
header: input.header,
|
|
227
|
+
summary: input.tabs,
|
|
228
|
+
body: side === undefined ? body : composeRows(theme, width, body, side),
|
|
229
|
+
detail: detail.length > 0 ? detail : undefined,
|
|
230
|
+
hint,
|
|
231
|
+
});
|
|
232
|
+
return { lines, noteShown, bodyRows: target };
|
|
196
233
|
}
|