@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
|
@@ -13,18 +13,18 @@
|
|
|
13
13
|
|
|
14
14
|
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
15
15
|
import type { Component, TuiMouseEvent, TuiMouseEventResult } from "@earendil-works/pi-tui";
|
|
16
|
-
import { clip } from "
|
|
17
|
-
import { detailBlockRows, dockListFrame, screenHint, type ViewportRowsSource } from "
|
|
18
|
-
import { sanitizeLine } from "
|
|
19
|
-
import { fitRows, formatRange, isCloseKey, wheelDelta } from "
|
|
20
|
-
import { SEPARATORS } from "
|
|
21
|
-
import { LinePane } from "
|
|
22
|
-
import { checkbox, emptyState, listRow } from "
|
|
23
|
-
import { titleRule } from "
|
|
24
|
-
import { paint, type TuiRole } from "
|
|
16
|
+
import { clip } from "../../../lib/tui/columns.ts";
|
|
17
|
+
import { detailBlockRows, dockListFrame, screenHint, type ViewportRowsSource } from "../../../lib/tui/chrome.ts";
|
|
18
|
+
import { sanitizeLine } from "../../../lib/format.ts";
|
|
19
|
+
import { fitRows, formatRange, isCloseKey, wheelDelta } from "../../../lib/tui/cursor.ts";
|
|
20
|
+
import { SEPARATORS } from "../../../lib/tui/glyphs.ts";
|
|
21
|
+
import { LinePane } from "../../../lib/tui/line-pane.ts";
|
|
22
|
+
import { checkbox, emptyState, listRow } from "../../../lib/tui/rows.ts";
|
|
23
|
+
import { titleRule } from "../../../lib/tui/rules.ts";
|
|
24
|
+
import { paint, type TuiRole } from "../../../lib/tui/tokens.ts";
|
|
25
25
|
import { ChecklistScreen } from "./checklist.ts";
|
|
26
|
-
import { formatReviewDetail, formatReviewEntryCounts, formatReviewSummary } from "
|
|
27
|
-
import { SELECTION_MARKS, type PathChange, type ReviewEntry } from "
|
|
26
|
+
import { formatReviewDetail, formatReviewEntryCounts, formatReviewSummary } from "../text/format.ts";
|
|
27
|
+
import { SELECTION_MARKS, type PathChange, type ReviewEntry } from "../text/paths.ts";
|
|
28
28
|
|
|
29
29
|
/** Rows a diff screen shows when the terminal height is unknown. */
|
|
30
30
|
const DIFF_DEFAULT_ITEMS = 20;
|
|
@@ -10,15 +10,15 @@
|
|
|
10
10
|
|
|
11
11
|
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
12
12
|
import { Key, matchesKey, type Component, type TuiMouseEvent, type TuiMouseEventResult } from "@earendil-works/pi-tui";
|
|
13
|
-
import { fitRows, formatRange, isCloseKey, ListCursor, navigateList, scrollList } from "
|
|
14
|
-
import { detailBlockRows, dockListFrame, screenHint, type ViewportRowsSource } from "
|
|
15
|
-
import { clip } from "
|
|
16
|
-
import { SEPARATORS } from "
|
|
17
|
-
import { emptyState, listRow } from "
|
|
18
|
-
import { titleRule } from "
|
|
19
|
-
import { paint } from "
|
|
20
|
-
import { formatRewindDetail, formatRewindParts } from "
|
|
21
|
-
import type { RewindPoint } from "
|
|
13
|
+
import { fitRows, formatRange, isCloseKey, ListCursor, navigateList, scrollList } from "../../../lib/tui/cursor.ts";
|
|
14
|
+
import { detailBlockRows, dockListFrame, screenHint, type ViewportRowsSource } from "../../../lib/tui/chrome.ts";
|
|
15
|
+
import { clip } from "../../../lib/tui/columns.ts";
|
|
16
|
+
import { SEPARATORS } from "../../../lib/tui/glyphs.ts";
|
|
17
|
+
import { emptyState, listRow } from "../../../lib/tui/rows.ts";
|
|
18
|
+
import { titleRule } from "../../../lib/tui/rules.ts";
|
|
19
|
+
import { paint } from "../../../lib/tui/tokens.ts";
|
|
20
|
+
import { formatRewindDetail, formatRewindParts } from "../text/format.ts";
|
|
21
|
+
import type { RewindPoint } from "../state/timeline.ts";
|
|
22
22
|
|
|
23
23
|
export interface RewindListOptions {
|
|
24
24
|
points: RewindPoint[];
|
|
@@ -57,7 +57,7 @@ export class RewindListComponent implements Component {
|
|
|
57
57
|
}
|
|
58
58
|
const count = this.points.length;
|
|
59
59
|
if (navigateList(data, this.cursor, count)) return;
|
|
60
|
-
if (matchesKey(data, Key.enter)
|
|
60
|
+
if (matchesKey(data, Key.enter)) {
|
|
61
61
|
const point = this.points[this.cursor.selected];
|
|
62
62
|
if (point) this.options.onClose(point);
|
|
63
63
|
}
|
|
@@ -139,6 +139,12 @@ export class RewindListComponent implements Component {
|
|
|
139
139
|
|
|
140
140
|
/** The key-hint row, with the primary action in the accent role. */
|
|
141
141
|
private hint(width: number): string {
|
|
142
|
-
return screenHint(this.theme, width, [
|
|
142
|
+
return screenHint(this.theme, width, [
|
|
143
|
+
"Esc close",
|
|
144
|
+
{ text: "Enter choose", color: "accent" },
|
|
145
|
+
"↑/↓ or k/j select",
|
|
146
|
+
"PgUp/PgDn",
|
|
147
|
+
"g/G or Home/End",
|
|
148
|
+
]);
|
|
143
149
|
}
|
|
144
150
|
}
|
|
@@ -8,10 +8,10 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
11
|
-
import { GLYPHS, STATUS_KEYS } from "
|
|
12
|
-
import { paint } from "
|
|
13
|
-
import type { RewindConfig } from "
|
|
14
|
-
import { userMessagesFromBranch } from "
|
|
11
|
+
import { GLYPHS, STATUS_KEYS } from "../../../lib/tui/glyphs.ts";
|
|
12
|
+
import { paint } from "../../../lib/tui/tokens.ts";
|
|
13
|
+
import type { RewindConfig } from "../config/config.ts";
|
|
14
|
+
import { userMessagesFromBranch } from "../state/timeline.ts";
|
|
15
15
|
|
|
16
16
|
const STATUS_KEY = STATUS_KEYS.rewind;
|
|
17
17
|
const STATUS_GLYPH = GLYPHS.rewind;
|
|
@@ -1,25 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The `/sessions` command.
|
|
2
|
+
* The `/sessions` command: one act, no arguments.
|
|
3
3
|
*
|
|
4
|
-
* In the TUI
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* the
|
|
8
|
-
*
|
|
4
|
+
* In the TUI it opens the live board: `Tab` cycles Sessions, Inbox, and Claims,
|
|
5
|
+
* and `w`/`x` act on this session's own watch and claims. Every other mode prints
|
|
6
|
+
* the compact listing, because there is no dock to mount the screen in. Reading is
|
|
7
|
+
* not joining, so the board is empty until this session joins — and a command
|
|
8
|
+
* context cannot name a session, so the join is `session({action:"join"})`,
|
|
9
|
+
* `/team join [<label>]`, or the board's `w`.
|
|
9
10
|
*/
|
|
10
11
|
|
|
11
12
|
import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
12
13
|
import { messageOf } from "../../lib/errors.ts";
|
|
13
14
|
import { sanitizeLine } from "../../lib/format.ts";
|
|
14
15
|
import { withPolledScreen } from "../../lib/screen-session.ts";
|
|
15
|
-
import {
|
|
16
|
+
import { formatJoin, formatStatus } from "./format.ts";
|
|
16
17
|
import { COMMAND_NAME } from "./protocol.ts";
|
|
17
18
|
import type { SessionBus } from "./runtime.ts";
|
|
18
19
|
import { SessionListComponent } from "./tui.ts";
|
|
19
20
|
|
|
20
|
-
const USAGE = "/sessions [name <label>|watch [off|wake]]";
|
|
21
|
-
const WATCH_MODES = ["off", "wake"];
|
|
22
|
-
|
|
23
21
|
/** How often the open board re-reads the bus. */
|
|
24
22
|
const BOARD_POLL_MS = 1_000;
|
|
25
23
|
|
|
@@ -48,7 +46,7 @@ function createBoardReader(bus: SessionBus): (screen: SessionListComponent) => P
|
|
|
48
46
|
}
|
|
49
47
|
|
|
50
48
|
/** Open the TUI board, and resolve when it closes. */
|
|
51
|
-
async function
|
|
49
|
+
async function openSessionsScreen(pi: ExtensionAPI, ctx: ExtensionCommandContext, bus: SessionBus): Promise<void> {
|
|
52
50
|
await bus.start(ctx);
|
|
53
51
|
const [status, messages] = await Promise.all([bus.snapshot(), bus.peek()]);
|
|
54
52
|
const first = { status, messages };
|
|
@@ -67,9 +65,10 @@ async function openBoard(pi: ExtensionAPI, ctx: ExtensionCommandContext, bus: Se
|
|
|
67
65
|
theme: screen.theme,
|
|
68
66
|
callbacks: {
|
|
69
67
|
toggleWatch: async (armed) => {
|
|
70
|
-
|
|
68
|
+
// The board's toggle is the same act as a join, so it says the same thing.
|
|
69
|
+
const joined = await bus.join({ listen: !armed });
|
|
71
70
|
if (holder.screen) await reader(holder.screen);
|
|
72
|
-
return
|
|
71
|
+
return formatJoin(joined.label, joined.watching);
|
|
73
72
|
},
|
|
74
73
|
releaseClaim: async (path) => {
|
|
75
74
|
const released = await bus.releaseStored(path);
|
|
@@ -92,7 +91,7 @@ async function openBoard(pi: ExtensionAPI, ctx: ExtensionCommandContext, bus: Se
|
|
|
92
91
|
async function open(ctx: ExtensionCommandContext, pi: ExtensionAPI, bus: SessionBus): Promise<void> {
|
|
93
92
|
try {
|
|
94
93
|
if (ctx.mode === "tui") {
|
|
95
|
-
await
|
|
94
|
+
await openSessionsScreen(pi, ctx, bus);
|
|
96
95
|
return;
|
|
97
96
|
}
|
|
98
97
|
await bus.start(ctx);
|
|
@@ -104,61 +103,10 @@ async function open(ctx: ExtensionCommandContext, pi: ExtensionAPI, bus: Session
|
|
|
104
103
|
|
|
105
104
|
export function registerCommands(pi: ExtensionAPI, bus: SessionBus): void {
|
|
106
105
|
pi.registerCommand(COMMAND_NAME, {
|
|
107
|
-
description: "
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
const options = ["name", "help", "watch"].filter((value) => value.startsWith(text));
|
|
112
|
-
return options.length > 0 ? options.map((value) => ({ value, label: value })) : null;
|
|
113
|
-
},
|
|
114
|
-
handler: async (args, ctx) => {
|
|
115
|
-
const [sub = "", ...rest] = args.trim().split(/\s+/).filter(Boolean);
|
|
116
|
-
if (sub === "help") {
|
|
117
|
-
ctx.ui.notify(
|
|
118
|
-
`/sessions — open the bus board\n/sessions name <label> — name this session\n` +
|
|
119
|
-
`/sessions watch [off|wake] — report new mail at the next turn, or raise one\n\n${USAGE}`,
|
|
120
|
-
"info",
|
|
121
|
-
);
|
|
122
|
-
return;
|
|
123
|
-
}
|
|
124
|
-
if (sub === "watch") {
|
|
125
|
-
const mode = rest[0] ?? "";
|
|
126
|
-
if (mode !== "" && !WATCH_MODES.includes(mode)) {
|
|
127
|
-
ctx.ui.notify(`Unknown watch mode "${mode}". Usage: /sessions watch [off|wake]`, "warning");
|
|
128
|
-
return;
|
|
129
|
-
}
|
|
130
|
-
try {
|
|
131
|
-
await bus.start(ctx);
|
|
132
|
-
const status = await bus.status();
|
|
133
|
-
const watching = await bus.watch({ stop: mode === "off", wake: mode === "wake" });
|
|
134
|
-
ctx.ui.notify(formatWatch(watching, status.me.label), "info");
|
|
135
|
-
} catch (error) {
|
|
136
|
-
ctx.ui.notify(`Session bus: ${messageOf(error)}`, "warning");
|
|
137
|
-
}
|
|
138
|
-
return;
|
|
139
|
-
}
|
|
140
|
-
if (sub === "name") {
|
|
141
|
-
const label = rest.join(" ").trim();
|
|
142
|
-
if (!label) {
|
|
143
|
-
ctx.ui.notify("Usage: /sessions name <label>", "warning");
|
|
144
|
-
return;
|
|
145
|
-
}
|
|
146
|
-
try {
|
|
147
|
-
await bus.start(ctx);
|
|
148
|
-
await bus.status();
|
|
149
|
-
const renamed = await bus.rename(label);
|
|
150
|
-
// The bus validates first, so a rejected label leaves both names alone.
|
|
151
|
-
pi.setSessionName(renamed);
|
|
152
|
-
ctx.ui.notify(`This session is now named "${renamed}".`, "info");
|
|
153
|
-
} catch (error) {
|
|
154
|
-
ctx.ui.notify(`Session bus: ${messageOf(error)}`, "warning");
|
|
155
|
-
}
|
|
156
|
-
return;
|
|
157
|
-
}
|
|
158
|
-
if (sub !== "") {
|
|
159
|
-
ctx.ui.notify(`Unknown sessions command "${sub}". Try ${USAGE}.`, "warning");
|
|
160
|
-
return;
|
|
161
|
-
}
|
|
106
|
+
description: "Open the bus board: sessions, unread mail, and path claims",
|
|
107
|
+
// One command, one act: an argument is not a sub-command, so it is ignored
|
|
108
|
+
// rather than warned about — the board is what the user asked for.
|
|
109
|
+
handler: async (_args, ctx) => {
|
|
162
110
|
await open(ctx, pi, bus);
|
|
163
111
|
},
|
|
164
112
|
});
|
|
@@ -7,8 +7,15 @@
|
|
|
7
7
|
|
|
8
8
|
import { sanitizeLine } from "../../lib/format.ts";
|
|
9
9
|
import type { BusMessage, ClaimConflict, ClaimRecord, SessionStatus } from "./types.ts";
|
|
10
|
-
/**
|
|
11
|
-
|
|
10
|
+
/**
|
|
11
|
+
* The last few characters of a peer's id, as the transcript shows it.
|
|
12
|
+
*
|
|
13
|
+
* Neutralized like every other peer-supplied field, and deliberately not
|
|
14
|
+
* `lib/session-label.ts`'s `shortId`: that one keeps the id's alphanumerics to
|
|
15
|
+
* build a default *label*, while this one is the id's own tail, sanitized for a
|
|
16
|
+
* row.
|
|
17
|
+
*/
|
|
18
|
+
export function idSuffix(id: string): string {
|
|
12
19
|
return sanitizeLine(id.slice(-6));
|
|
13
20
|
}
|
|
14
21
|
|
|
@@ -22,50 +29,35 @@ export function formatStatus(status: SessionStatus): string {
|
|
|
22
29
|
const unread = session.unread > 0 ? ` · ${session.unread} unread` : "";
|
|
23
30
|
const label = sanitizeLine(session.label);
|
|
24
31
|
const cwd = sanitizeLine(session.cwd);
|
|
25
|
-
return `- ${label} (${
|
|
32
|
+
return `- ${label} (${idSuffix(session.id)}) · ${cwd} · ${sanitizeLine(session.mode)}${unread}`;
|
|
26
33
|
});
|
|
27
34
|
const claims = status.claims.map((claim) => `- ${claimLine(claim)}`);
|
|
28
35
|
const alerts = status.watching ? [`Alerts: inbox watch on${status.watching.wake ? " (wake)" : ""}`] : [];
|
|
29
|
-
const units = status.units ? [unitLine(status.units)] : [];
|
|
30
36
|
return [
|
|
31
37
|
`Sessions (${status.sessions.length})`,
|
|
32
38
|
...(sessions.length > 0 ? sessions : ["- none"]),
|
|
33
39
|
"",
|
|
34
40
|
`Claims (${status.claims.length})`,
|
|
35
41
|
...(claims.length > 0 ? claims : ["- none"]),
|
|
36
|
-
...(units.length > 0 ? ["", ...units] : []),
|
|
37
42
|
...(alerts.length > 0 ? ["", ...alerts] : []),
|
|
38
43
|
"",
|
|
39
|
-
`You: ${sanitizeLine(status.me.label)} (${
|
|
44
|
+
`You: ${sanitizeLine(status.me.label)} (${idSuffix(status.me.id)})${status.onBus ? "" : " \u00b7 not on the bus yet \u2014 `session join` puts you on it"}`,
|
|
40
45
|
].join("\n");
|
|
41
46
|
}
|
|
42
47
|
|
|
43
48
|
/**
|
|
44
|
-
* The
|
|
45
|
-
*
|
|
49
|
+
* The join result: the label peers address, and whether mail will reach this session.
|
|
50
|
+
*
|
|
51
|
+
* One sentence for one act, because `join` is one action — the name and the watch are
|
|
52
|
+
* two facts about the same decision, and a reader that had to ask twice would be reading
|
|
53
|
+
* a bus the user is not supposed to operate.
|
|
46
54
|
*/
|
|
47
|
-
function
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
);
|
|
51
|
-
return [
|
|
52
|
-
`Units ${units.joined}/${units.total} joined · ${sanitizeLine(units.team)}`,
|
|
53
|
-
...rows.map((row) => `- ${row}`),
|
|
54
|
-
].join("\n");
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/** The rename result: the label the bus settled on, which peers now address. */
|
|
58
|
-
export function formatName(label: string): string {
|
|
59
|
-
return `This session is now named "${sanitizeLine(label)}".`;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/** The inbox-watch result. */
|
|
63
|
-
export function formatWatch(watching: { armed: boolean; wake: boolean }, name?: string): string {
|
|
64
|
-
const owner = name ? ` for "${sanitizeLine(name)}"` : "";
|
|
65
|
-
if (!watching.armed) return `Inbox watch off${owner}.`;
|
|
55
|
+
export function formatJoin(label: string, watching: { armed: boolean; wake: boolean }): string {
|
|
56
|
+
const who = `On the bus as "${sanitizeLine(label)}"`;
|
|
57
|
+
if (!watching.armed) return `${who}, not listening: mail waits in the inbox.`;
|
|
66
58
|
return watching.wake
|
|
67
|
-
?
|
|
68
|
-
:
|
|
59
|
+
? `${who}, listening: a new message raises one turn.`
|
|
60
|
+
: `${who}, listening: new messages appear at your next turn.`;
|
|
69
61
|
}
|
|
70
62
|
|
|
71
63
|
/** One consumed inbox, oldest first. */
|
|
@@ -95,26 +87,11 @@ export function formatClaim(claimed: readonly string[], conflicts: readonly Clai
|
|
|
95
87
|
if (claimed.length > 0) lines.push(`Claimed: ${claimed.map((path) => sanitizeLine(path)).join(", ")}.`);
|
|
96
88
|
for (const conflict of conflicts) {
|
|
97
89
|
const owner = sanitizeLine(conflict.ownerLabel);
|
|
98
|
-
lines.push(`${sanitizeLine(conflict.path)} is owned by ${owner} (${
|
|
90
|
+
lines.push(`${sanitizeLine(conflict.path)} is owned by ${owner} (${idSuffix(conflict.owner)}).`);
|
|
99
91
|
}
|
|
100
92
|
return lines.length > 0 ? lines.join("\n") : "Nothing claimed.";
|
|
101
93
|
}
|
|
102
94
|
|
|
103
|
-
/** Join result: which unit, and what it claimed or could not. */
|
|
104
|
-
export function formatJoin(
|
|
105
|
-
team: string,
|
|
106
|
-
unit: string,
|
|
107
|
-
claimed: readonly string[],
|
|
108
|
-
conflicts: readonly ClaimConflict[],
|
|
109
|
-
): string {
|
|
110
|
-
const where = `${sanitizeLine(team)}/${sanitizeLine(unit)}`;
|
|
111
|
-
if (conflicts.length > 0) {
|
|
112
|
-
return [`Could not join ${where}.`, ...formatClaim([], conflicts).split("\n").filter(Boolean)].join("\n");
|
|
113
|
-
}
|
|
114
|
-
const paths = claimed.map((path) => sanitizeLine(path)).join(", ");
|
|
115
|
-
return claimed.length > 0 ? `Joined ${where}: claimed ${paths}.` : `Joined ${where}: nothing to claim.`;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
95
|
/** Release result. */
|
|
119
96
|
export function formatRelease(released: readonly string[], notOwned: readonly string[]): string {
|
|
120
97
|
const lines: string[] = [];
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* session-bus — coordinate parallel Pi sessions on one repository.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
4
|
+
* No session joins on start: it joins through `join`, through the first action
|
|
5
|
+
* that makes it addressable, or through `w` on its own board, in any mode.
|
|
6
|
+
* `/sessions` and `status` only read, so a fresh board is empty and a peer never
|
|
7
|
+
* appears merely because the package loaded. The factory only registers; the
|
|
8
|
+
* runtime owns the heartbeat and removes the registry entry on shutdown. Nothing
|
|
9
|
+
* arms an inbox watch by itself, so delivery stays pull-based until a session
|
|
10
|
+
* asks: with the watch armed, new mail is reported at the next turn, or — with
|
|
11
|
+
* `wake` — raises one turn as soon as the agent is idle, including right after a
|
|
12
|
+
* running turn settles.
|
|
11
13
|
*
|
|
12
14
|
* Load with: pi --extension ./extensions/session-bus
|
|
13
15
|
*/
|
|
@@ -17,6 +19,7 @@ import { isExtensionEnabled } from "../../lib/env.ts";
|
|
|
17
19
|
import { messageOf } from "../../lib/errors.ts";
|
|
18
20
|
import { createPendingReporter, registerPendingChannel } from "../../lib/report.ts";
|
|
19
21
|
import { registerCommands } from "./commands.ts";
|
|
22
|
+
import { registerJoinRequests } from "./join-request.ts";
|
|
20
23
|
import { createSessionBus, type SessionBus } from "./runtime.ts";
|
|
21
24
|
import { registerTools } from "./tools.ts";
|
|
22
25
|
import { mailNotice } from "./notice.ts";
|
|
@@ -42,6 +45,9 @@ export default function sessionBus(pi: ExtensionAPI, deps: SessionBusDeps = {}):
|
|
|
42
45
|
|
|
43
46
|
registerTools(pi, bus);
|
|
44
47
|
registerCommands(pi, bus);
|
|
48
|
+
// `board` and `team` join this session through one request: their commands cannot call
|
|
49
|
+
// a tool, and the user's surface for the team is theirs, not this extension's.
|
|
50
|
+
registerJoinRequests(pi, bus);
|
|
45
51
|
|
|
46
52
|
registerPendingChannel(pi, {
|
|
47
53
|
reporter,
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The join request, answered.
|
|
3
|
+
*
|
|
4
|
+
* `board` and `team` sit above the bus: `/board`, `/team join`, and a card write all
|
|
5
|
+
* put this session on the team, and none of them may import this extension. A *tool*
|
|
6
|
+
* context could reach the `session` tool and an `ExtensionCommandContext` cannot —
|
|
7
|
+
* the same limit `lib/plan-entry.ts` works around for plan mode — so the request
|
|
8
|
+
* travels on `pi.events` and this module answers it with the runtime's own `join`.
|
|
9
|
+
*
|
|
10
|
+
* The answer always comes: every path here ends in `respond`, including a failure, so
|
|
11
|
+
* a caller never waits on a listener that threw. The label also becomes Pi's session
|
|
12
|
+
* name, because a team label and the footer have to agree.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
16
|
+
import { messageOf } from "../../lib/errors.ts";
|
|
17
|
+
import { type JoinResult, TEAM_JOIN_REQUEST, type TeamJoinRequest } from "../../lib/session-tool.ts";
|
|
18
|
+
import type { SessionBus } from "./runtime.ts";
|
|
19
|
+
|
|
20
|
+
export function registerJoinRequests(pi: ExtensionAPI, bus: SessionBus): void {
|
|
21
|
+
pi.events.on(TEAM_JOIN_REQUEST, (data) => {
|
|
22
|
+
const request = data as TeamJoinRequest | undefined;
|
|
23
|
+
if (!request || typeof request.respond !== "function") return;
|
|
24
|
+
void answer(request);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
async function answer(request: TeamJoinRequest): Promise<void> {
|
|
28
|
+
let result: JoinResult;
|
|
29
|
+
try {
|
|
30
|
+
await bus.start(request.ctx);
|
|
31
|
+
const joined = await bus.join(request.input);
|
|
32
|
+
// One identity: the bus's label is also the name Pi's footer and `/resume` show.
|
|
33
|
+
if (request.input.label !== undefined) pi.setSessionName(joined.label);
|
|
34
|
+
result = { ok: true, label: joined.label, watching: joined.watching };
|
|
35
|
+
} catch (error) {
|
|
36
|
+
result = { ok: false, error: messageOf(error) };
|
|
37
|
+
}
|
|
38
|
+
request.respond(result);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The armed inbox watch: polling this session's own inbox and the queue of mail
|
|
3
|
+
* that arrived and has not been reported yet.
|
|
4
|
+
*
|
|
5
|
+
* It announces and never consumes: `inbox` keeps sole ownership of deleting mail,
|
|
6
|
+
* so a wake cannot eat a message the model did not read. Nothing here knows about
|
|
7
|
+
* the store, the repository, or the transcript — the runtime supplies one read-only
|
|
8
|
+
* peek and the timer, and the extension's reporter supplies `onWake`.
|
|
9
|
+
*
|
|
10
|
+
* Polling rather than `fs.watch`, like the `watch` extension's tail: one readdir on
|
|
11
|
+
* this session's own directory stands in for a platform event API.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type { RepeatTimer } from "../../lib/timer.ts";
|
|
15
|
+
import type { BusMessage } from "./types.ts";
|
|
16
|
+
|
|
17
|
+
export interface MailWatchOptions {
|
|
18
|
+
/** This session's unread mail, read-only: nothing is consumed or marked. */
|
|
19
|
+
peek(): Promise<BusMessage[]>;
|
|
20
|
+
/** The repeating timer behind the poll. */
|
|
21
|
+
interval: RepeatTimer;
|
|
22
|
+
/** Poll interval; 0 disables the ticker (tests). */
|
|
23
|
+
watchMs: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Whether the watch is on, and whether it raises a turn. */
|
|
27
|
+
export interface WatchingState {
|
|
28
|
+
armed: boolean;
|
|
29
|
+
wake: boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface MailWatch {
|
|
33
|
+
/** Arm or disarm this session's inbox watch. */
|
|
34
|
+
watch(input: { wake?: boolean; stop?: boolean }): Promise<WatchingState>;
|
|
35
|
+
/** The current state, for the status line. */
|
|
36
|
+
state(): WatchingState;
|
|
37
|
+
/** Whether the armed watch asked for a triggered turn; the settle retry reads it. */
|
|
38
|
+
wantsWake(): boolean;
|
|
39
|
+
/** Read the inbox without consuming it, queueing what is new; no-op when unarmed. */
|
|
40
|
+
poll(): Promise<void>;
|
|
41
|
+
/** Mail that arrived and has not been reported yet, oldest first. */
|
|
42
|
+
pending(): BusMessage[];
|
|
43
|
+
/** Drop mail from the pending queue, once reported or consumed. */
|
|
44
|
+
markReported(ids: readonly string[]): void;
|
|
45
|
+
/** Release the ticker and the queue, on shutdown. */
|
|
46
|
+
stop(): void;
|
|
47
|
+
set onWake(callback: (() => void) | undefined);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function createMailWatch(options: MailWatchOptions): MailWatch {
|
|
51
|
+
const { peek, interval, watchMs } = options;
|
|
52
|
+
|
|
53
|
+
let armed = false;
|
|
54
|
+
let wake = false;
|
|
55
|
+
let stopTicker: (() => void) | undefined;
|
|
56
|
+
let pendingMail: BusMessage[] = [];
|
|
57
|
+
/** Ids already queued, reported, or consumed; a broadcast shares one id. */
|
|
58
|
+
let reported = new Set<string>();
|
|
59
|
+
let onWake: (() => void) | undefined;
|
|
60
|
+
|
|
61
|
+
function dropFromPending(ids: ReadonlySet<string>): void {
|
|
62
|
+
pendingMail = pendingMail.filter((message) => !ids.has(message.id));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function markReported(ids: readonly string[]): void {
|
|
66
|
+
for (const id of ids) reported.add(id);
|
|
67
|
+
dropFromPending(new Set(ids));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Queue what arrived since the last poll, once, and wake a turn when this
|
|
72
|
+
* session asked for one.
|
|
73
|
+
*
|
|
74
|
+
* The reported set is narrowed to the mail still on disk, so it is bounded by
|
|
75
|
+
* the unread count rather than by the session's whole history; an id is unique
|
|
76
|
+
* for good, so a message that was consumed can never be queued again.
|
|
77
|
+
*/
|
|
78
|
+
async function pollOnce(): Promise<void> {
|
|
79
|
+
if (!armed) return;
|
|
80
|
+
const mail = await peek();
|
|
81
|
+
const unread = new Set(mail.map((message) => message.id));
|
|
82
|
+
reported = new Set([...reported].filter((id) => unread.has(id)));
|
|
83
|
+
// A message that expired or was consumed leaves the queue with its file.
|
|
84
|
+
pendingMail = pendingMail.filter((message) => unread.has(message.id));
|
|
85
|
+
const queued = new Set(pendingMail.map((message) => message.id));
|
|
86
|
+
const fresh = mail.filter((message) => !reported.has(message.id) && !queued.has(message.id));
|
|
87
|
+
if (fresh.length === 0) return;
|
|
88
|
+
pendingMail = [...pendingMail, ...fresh];
|
|
89
|
+
if (wake) onWake?.();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function disarm(): void {
|
|
93
|
+
stopTicker?.();
|
|
94
|
+
stopTicker = undefined;
|
|
95
|
+
armed = false;
|
|
96
|
+
wake = false;
|
|
97
|
+
pendingMail = [];
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function watch(input: { wake?: boolean; stop?: boolean }): Promise<WatchingState> {
|
|
101
|
+
if (input.stop) {
|
|
102
|
+
disarm();
|
|
103
|
+
return { armed: false, wake: false };
|
|
104
|
+
}
|
|
105
|
+
if (!armed) {
|
|
106
|
+
armed = true;
|
|
107
|
+
wake = input.wake === true;
|
|
108
|
+
await pollOnce();
|
|
109
|
+
if (watchMs > 0) stopTicker = interval(watchMs, () => void pollOnce().catch(() => {}));
|
|
110
|
+
return { armed, wake };
|
|
111
|
+
}
|
|
112
|
+
// The mode is a setting, not an arm-time flag: an explicit `wake` changes it
|
|
113
|
+
// under a queued notice, and an omitted one keeps it, so bare `watch` never
|
|
114
|
+
// demotes an armed session.
|
|
115
|
+
if (input.wake !== undefined) wake = input.wake;
|
|
116
|
+
// Asking for wake over mail already queued is a request for that burst too;
|
|
117
|
+
// mid-turn it is suppressed here and retried once the run settles.
|
|
118
|
+
if (wake && pendingMail.length > 0) onWake?.();
|
|
119
|
+
return { armed, wake };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
watch,
|
|
124
|
+
state: () => ({ armed, wake }),
|
|
125
|
+
wantsWake: () => armed && wake,
|
|
126
|
+
poll: pollOnce,
|
|
127
|
+
pending: () => [...pendingMail],
|
|
128
|
+
markReported,
|
|
129
|
+
stop: disarm,
|
|
130
|
+
set onWake(callback: (() => void) | undefined) {
|
|
131
|
+
onWake = callback;
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
}
|
|
@@ -20,10 +20,30 @@ const MAX_NOTICE_LINES = 5;
|
|
|
20
20
|
/** A message body is clipped to this many characters in a preview. */
|
|
21
21
|
const MAX_PREVIEW = 200;
|
|
22
22
|
|
|
23
|
+
/**
|
|
24
|
+
* A handoff is clipped far less generously, because it is an instruction and not a
|
|
25
|
+
* notification: the card, its paths, and the step to take all sit past the point
|
|
26
|
+
* where a message preview would have stopped. The inbox still holds the full text.
|
|
27
|
+
*/
|
|
28
|
+
const HANDOFF_PREVIEW = 800;
|
|
29
|
+
|
|
30
|
+
/** What a waking turn is told to do with a handoff: work it, not acknowledge it. */
|
|
31
|
+
const HANDOFF_HINT =
|
|
32
|
+
"A handoff is work you now own: run `session inbox`, record the card's plan, and start — do not just acknowledge it.";
|
|
33
|
+
|
|
34
|
+
/** `1 handoff`, `2 new messages` — a burst of handoffs is named as the work it is. */
|
|
35
|
+
function headlineOf(messages: readonly BusMessage[]): string {
|
|
36
|
+
const count = messages.length;
|
|
37
|
+
const handoffs = messages.filter((message) => message.kind === "handoff").length;
|
|
38
|
+
if (handoffs === 0 || handoffs !== count) return `${count} new message${count === 1 ? "" : "s"}`;
|
|
39
|
+
return `${count} handoff${count === 1 ? "" : "s"}`;
|
|
40
|
+
}
|
|
41
|
+
|
|
23
42
|
/** One message as a preview line: sender, kind, and the head of its text. */
|
|
24
43
|
function previewOf(message: BusMessage): string {
|
|
25
44
|
const text = sanitizeLine(message.text);
|
|
26
|
-
const
|
|
45
|
+
const limit = message.kind === "handoff" ? HANDOFF_PREVIEW : MAX_PREVIEW;
|
|
46
|
+
const head = text.length > limit ? `${text.slice(0, limit)}…` : text;
|
|
27
47
|
const paths = message.paths?.length ? ` (${message.paths.map((path) => sanitizeLine(path)).join(", ")})` : "";
|
|
28
48
|
return `${sanitizeLine(message.fromLabel)}${SEPARATORS.item}${sanitizeLine(message.kind)}${SEPARATORS.item}${head}${paths}`;
|
|
29
49
|
}
|
|
@@ -35,6 +55,11 @@ function previewOf(message: BusMessage): string {
|
|
|
35
55
|
* message is passed to the renderer, whose own cap draws the `… N more` row. The
|
|
36
56
|
* two caps are deliberately separate — one is a context budget, the other a
|
|
37
57
|
* screen height.
|
|
58
|
+
*
|
|
59
|
+
* A `handoff` is the one kind that assigns work, so it is the one kind the text
|
|
60
|
+
* treats as an instruction rather than as mail: the headline counts handoffs, the
|
|
61
|
+
* preview keeps enough of the body to be actionable, and the hint says to start it.
|
|
62
|
+
* Nothing else about the kind changes — it still does not block or wait for a reply.
|
|
38
63
|
*/
|
|
39
64
|
export function mailNotice(messages: BusMessage[]): NoticeMessage {
|
|
40
65
|
const count = messages.length;
|
|
@@ -45,14 +70,17 @@ export function mailNotice(messages: BusMessage[]): NoticeMessage {
|
|
|
45
70
|
detail: sanitizeLine(message.text).slice(0, MAX_PREVIEW),
|
|
46
71
|
severity: message.kind === "event" ? "warn" : "info",
|
|
47
72
|
}));
|
|
73
|
+
const headline = headlineOf(messages);
|
|
48
74
|
return noticeMessage({
|
|
49
75
|
content: burstText({
|
|
50
76
|
marker: BUS_CONTEXT_MARKER,
|
|
51
|
-
headline
|
|
77
|
+
headline,
|
|
52
78
|
lines: shown.map((message) => `- ${previewOf(message)}`),
|
|
53
79
|
...(more > 0 ? { more: `… ${more} more` } : {}),
|
|
54
|
-
hint:
|
|
80
|
+
hint: messages.some((message) => message.kind === "handoff")
|
|
81
|
+
? HANDOFF_HINT
|
|
82
|
+
: "Run `session inbox` to read and consume them.",
|
|
55
83
|
}),
|
|
56
|
-
details: { source: "session-bus", title:
|
|
84
|
+
details: { source: "session-bus", title: headline, items },
|
|
57
85
|
});
|
|
58
86
|
}
|