@huanlin/dsh-plugin-better-plan 0.4.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.
@@ -0,0 +1,35 @@
1
+ /**
2
+ * The Plan tab view: a status header (title, path, actions), the review
3
+ * action bar (the sidebar approval surface — the chat shows no popup), and
4
+ * the plan rendered through DSH's shared `MarkdownText`.
5
+ *
6
+ * The file content comes from better-sidebar's `/sidebar/api/fs.read` route
7
+ * (same-origin, browser-authenticated) — the plan file lives in the session
8
+ * workspace, and the host half has no route of its own for reading it. The
9
+ * path is `tab.meta.path` (persisted with the tab, so a refresh restores the
10
+ * view) falling back to `tab.path`.
11
+ *
12
+ * The review state comes from the delivery WebSocket's review frames (via
13
+ * the shared review store, restored by the attach replay after a refresh).
14
+ * While a review is pending the bar offers Approve / Execute in new chat /
15
+ * Keep planning; a decision POSTs to the host's review route and the echo
16
+ * updates the store. The delegation choice then launches the execution
17
+ * conversation through the sessions service (see execution-launch.ts) and
18
+ * navigates there.
19
+ *
20
+ * @module @huanlin/dsh-plugin-better-plan/client/PlanView
21
+ */
22
+ import { createElement } from 'react';
23
+ import type { TabComponentProps } from 'dsh-better-sidebar/client/service';
24
+ /**
25
+ * Extract the plan path from a tab (meta first, then the seed path).
26
+ * @param tab - the sidebar tab instance.
27
+ * @returns the plan file path, or undefined when the tab carries none.
28
+ */
29
+ export declare function planPathOf(tab: TabComponentProps['tab']): string | undefined;
30
+ /**
31
+ * The Plan tab component (better-sidebar TabDescriptor.component).
32
+ * @param props - the tab component props (ctx/store/scope/tab/visible).
33
+ * @returns the rendered view.
34
+ */
35
+ export declare function PlanView(props: TabComponentProps): ReturnType<typeof createElement>;
@@ -0,0 +1,65 @@
1
+ /**
2
+ * The delegation flow's client half: after a review settles as
3
+ * `delegated`, create the execution conversation through DSH's public
4
+ * sessions service (`ctx.get('sessions')`), queue the kickoff prompt into
5
+ * it, and navigate there.
6
+ *
7
+ * The faces below are STRUCTURAL (the minimal subset of
8
+ * `@deepseek-ai/dsh-api-session-controller/client`'s `ISessions` /
9
+ * `SessionFace` this flow calls) — the same pattern better-sidebar uses for
10
+ * cross-plugin services: no value import crosses the client-bundle purity
11
+ * gate and no new tsconfig path is needed; the runtime service arrives
12
+ * through the context proxy. The planning session's list summary supplies
13
+ * the new conversation's cwd, so the fresh agent runs on the same
14
+ * workspace the plan was written for.
15
+ *
16
+ * @module @huanlin/dsh-plugin-better-plan/client/execution-launch
17
+ */
18
+ /** One admitted prompt's outcome (the client-result face of `prompt`). */
19
+ export interface PromptOutcome {
20
+ ok: boolean;
21
+ error?: {
22
+ code?: string;
23
+ message?: string;
24
+ };
25
+ }
26
+ /** The structural session face the kickoff needs (the `prompt` verb). */
27
+ export interface ExecutionSessionFace {
28
+ prompt(content: {
29
+ type: 'text';
30
+ text: string;
31
+ }[], mode: 'queue'): Promise<PromptOutcome>;
32
+ }
33
+ /** The structural `ctx.sessions` face the delegation flow consumes. */
34
+ export interface SessionsServiceFace {
35
+ /** Create a blank session (in the workspace of `cwd` when given). */
36
+ create(opts?: {
37
+ cwd?: string;
38
+ }): Promise<string>;
39
+ /** Select a session as the current conversation. */
40
+ open(id: string): void;
41
+ /** The session list snapshot (the planning session's cwd source). */
42
+ list: {
43
+ getSnapshot(): {
44
+ byId: Record<string, {
45
+ cwd?: string;
46
+ } | undefined>;
47
+ };
48
+ };
49
+ /** The live session face for a locally addressable session. */
50
+ binding(id: string): {
51
+ session: ExecutionSessionFace;
52
+ } | undefined;
53
+ }
54
+ /**
55
+ * Launch the execution conversation for a delegated plan: create a blank
56
+ * session in the planning session's workspace, queue the kickoff prompt
57
+ * pointing at the approved plan file, then navigate to the new
58
+ * conversation. Every failure rejects with an Error whose message the
59
+ * plan panel shows (the plan path stays visible there, so the user can
60
+ * always start a conversation and send the plan manually).
61
+ * @param sessions - the sessions service (`ctx.get('sessions')`).
62
+ * @param planPath - the approved plan file's absolute path.
63
+ * @param sessionId - the planning session (its summary carries the cwd).
64
+ */
65
+ export declare function launchExecutionConversation(sessions: SessionsServiceFace, planPath: string, sessionId: string): Promise<void>;
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Inline SVG icon for the Plan tab (client bundles must not value-import
3
+ * other plugins' internals, and a one-glyph icon does not justify a
4
+ * react-icons dependency).
5
+ *
6
+ * @module @huanlin/dsh-plugin-better-plan/client/icons
7
+ */
8
+ import { createElement } from 'react';
9
+ /**
10
+ * The Plan tab glyph: a checklist document.
11
+ * @param props - size in pixels.
12
+ * @returns the icon element.
13
+ */
14
+ export declare function IconPlanOutline16(props: {
15
+ size: number;
16
+ }): ReturnType<typeof createElement>;
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Client half of @huanlin/dsh-plugin-better-plan: registers the sidebar's
3
+ * "Plan" tab through better-sidebar's service and subscribes the per-session
4
+ * delivery WebSocket that opens it and feeds its review action bar.
5
+ *
6
+ * The tab is `single: true` (one Plan tab per session, dedupe-focused on
7
+ * repeat deliveries). Because the dedupe focus does NOT overwrite an already
8
+ * open tab's path, every push is followed by `updateTab` (feature-gated,
9
+ * v0.12.0+) so a re-delivered plan replaces the tab's content, then
10
+ * `activateTab` focuses it. `meta` rides the tab into better-sidebar's
11
+ * localStorage persistence, so a refresh restores the view and PlanView
12
+ * re-reads the file from `tab.meta.path`. Review frames feed the shared
13
+ * review store; the tab's action bar posts decisions back to
14
+ * `POST /better-plan/api/review` — the sidebar IS the approval surface, the
15
+ * chat shows no popup.
16
+ *
17
+ * With better-sidebar absent this half stays pending on its inject (legal
18
+ * per the client runner) and, defensively, apply() skips everything when the
19
+ * service is unreachable — the host half's `delivered: false` path covers
20
+ * the no-sidebar deployment.
21
+ *
22
+ * @module @huanlin/dsh-plugin-better-plan/client
23
+ */
24
+ import type { Context } from '@deepseek-ai/cordis';
25
+ import type { BetterSidebarService } from 'dsh-better-sidebar/client/service';
26
+ import { ReviewStore } from './review-store.ts';
27
+ /** The Plan tab type id (also the minted tab id — single instance). */
28
+ export declare const TAB_ID = "better-plan:plan";
29
+ /** The delivery WebSocket path (mirror of the host half's route). */
30
+ export declare const DELIVERY_WS_PATH = "/better-plan/ws/delivery";
31
+ /**
32
+ * Apply one delivery push to the sidebar: open (or focus) the Plan tab, then
33
+ * overwrite its content via updateTab and focus it (both v0.12.0+; on an
34
+ * older host the plain openTab dedupe-focus still lands the FIRST delivery).
35
+ * @param service - the better-sidebar service.
36
+ * @param payload - the parsed WS frame.
37
+ * @param sessionId - the session the socket is subscribed to.
38
+ */
39
+ export declare function applyDeliveryPush(service: BetterSidebarService, payload: unknown, sessionId: string): void;
40
+ /**
41
+ * Route one WS frame: `deliver` opens/updates the Plan tab, `review` feeds
42
+ * the review store (the Plan tab's action bar). Malformed frames are ignored
43
+ * (the next push carries its own state).
44
+ * @param service - the better-sidebar service.
45
+ * @param store - the review store the review frames feed.
46
+ * @param frame - the parsed WS frame.
47
+ * @param sessionId - the session the socket is subscribed to.
48
+ */
49
+ export declare function applyDeliveryFrame(service: BetterSidebarService, store: ReviewStore, frame: unknown, sessionId: string): void;
50
+ /** The betterSidebar and locale services this half resolves through the context proxy. */
51
+ export declare const inject: string[];
52
+ /**
53
+ * Client plugin body.
54
+ * @param ctx - the client cordis context.
55
+ */
56
+ export declare function apply(ctx: Context): void;
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Client-half copy for the Plan tab, following the DSH i18n system: the
3
+ * dictionaries register into the shared locale registry (namespace
4
+ * `betterPlan`), and `t()` resolves the active locale from the attached
5
+ * `ctx.locale` service (`@deepseek-ai/dsh-client-locale`) — the Host-backed
6
+ * `locale.preference` wins over the raw browser language and switches live.
7
+ * Absent the service (locale plugin not mounted), the browser language is
8
+ * the fallback.
9
+ *
10
+ * The module-level attach mirrors better-sidebar's own locales.ts: the Plan
11
+ * tab renders inside the sidebar's React tree, so the component reads copy
12
+ * through `t()` at render time; the sidebar re-renders tab content on locale
13
+ * switches (its tab-content memo keys on the locale revision), so no extra
14
+ * subscription is needed here.
15
+ *
16
+ * @module @huanlin/dsh-plugin-better-plan/client/locales
17
+ */
18
+ /** The zh dictionary (source of truth for the key set). */
19
+ export declare const zhDict: {
20
+ readonly tabTitle: "计划";
21
+ readonly openInEditor: "在编辑器中打开";
22
+ readonly copyPath: "复制路径";
23
+ readonly copied: "已复制";
24
+ readonly copy: "复制";
25
+ readonly copiedLabel: "已复制";
26
+ readonly reviewHint: "在此审阅计划——聊天中不会弹出审批卡。点「批准」在本对话执行,点「新开对话执行」移到全新对话执行,或附反馈选择「继续规划」。";
27
+ readonly feedbackPlaceholder: "「继续规划」时可附反馈(可选)…";
28
+ readonly approve: "批准";
29
+ readonly keepPlanning: "继续规划";
30
+ readonly approveNewSession: "新开对话执行";
31
+ readonly delegatingStatus: "计划已批准——正在新建执行对话…";
32
+ readonly delegatedStatus: "计划已批准——执行已移交到新对话。";
33
+ readonly errDelegateFailed: "新开执行对话失败";
34
+ readonly approvedStatus: "计划已批准——模型正在执行该计划。";
35
+ readonly keptStatus: "反馈已发送——模型正在修改计划。";
36
+ readonly loading: "正在读取计划…";
37
+ readonly readFailed: "读取计划失败";
38
+ readonly retry: "重试";
39
+ readonly truncated: "文件因侧边栏读取上限被截断;其余内容请在编辑器中查看。";
40
+ readonly noPath: "该计划标签页未携带文件路径";
41
+ readonly binaryFile: "计划文件是二进制文件;请在编辑器中查看";
42
+ readonly unexpectedRead: "fs.read 响应格式异常";
43
+ readonly errStale: "当前面板已过期——有更新的计划正在审批。";
44
+ readonly errNoPending: "当前没有等待审批的计划。";
45
+ readonly errSubmitFailed: "提交决定失败";
46
+ };
47
+ /** The en dictionary (key-set-equal to zh, enforced by the type annotation). */
48
+ export declare const enDict: Record<keyof typeof zhDict, string>;
49
+ /** The dictionary type every future locale must satisfy. */
50
+ export type PlanCopy = Record<keyof typeof zhDict, string>;
51
+ /** The locale namespace this plugin owns in the DSH locale registry. */
52
+ export declare const LOCALE_NS = "betterPlan";
53
+ /** The minimal face of the DSH locale service this module needs. */
54
+ interface LocaleServiceFace {
55
+ getSnapshot(): {
56
+ active: string;
57
+ };
58
+ }
59
+ /**
60
+ * Attach (or detach, with undefined) the DSH locale service.
61
+ * @param service - the client context's locale service.
62
+ */
63
+ export declare function attachLocale(service: LocaleServiceFace | undefined): void;
64
+ /**
65
+ * The active copy locale: the DSH locale service's snapshot when attached
66
+ * (zh → zh, anything else → en), else the browser language.
67
+ * @returns `'zh'` or `'en'`.
68
+ */
69
+ export declare function activeLocale(): 'zh' | 'en';
70
+ /**
71
+ * Translate one copy key in the active locale.
72
+ * @param key - the copy key.
73
+ * @returns the localized string.
74
+ */
75
+ export declare function t(key: keyof PlanCopy): string;
76
+ /**
77
+ * The kickoff prompt the delegation flow queues into the NEW conversation:
78
+ * the model there has no context, so the message anchors it on the approved
79
+ * plan file (an absolute path) and orders execution. It is both
80
+ * user-visible (a chat bubble) and model-directed, so it localizes with the
81
+ * view like the steer copy does.
82
+ * @param path - the absolute plan file path.
83
+ * @returns the kickoff prompt text.
84
+ */
85
+ export declare function executionKickoffPrompt(path: string): string;
86
+ /**
87
+ * Localize one review-route error for the action bar: known error codes map
88
+ * to copy; unknown codes fall back to the route's raw English message.
89
+ * @param code - the route's stable error code, when present.
90
+ * @param raw - the route's raw error message (or an HTTP fallback).
91
+ * @returns the text the action bar shows.
92
+ */
93
+ export declare function submitErrorText(code: string | undefined, raw: string): string;
94
+ export {};
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Chrome labels for DSH's shared `MarkdownText`, shaped for BOTH prop
3
+ * generations the plugin supports (inlined from the sidebar's
4
+ * markdown-labels helper — client bundles must not value-import other
5
+ * plugins' internals):
6
+ *
7
+ * - 0.1.1-rc.x: optional flat prop `codeLabels` — the renderer reads
8
+ * `labels.copyLabel` / `labels.copiedLabel` directly.
9
+ * - 0.1.2-alpha.1+: a REQUIRED nested `labels` prop
10
+ * (`labels.code.copyLabel` + a screen-reader-only `labels.footnotes`
11
+ * heading). Passing only the old prop crashes the fence render with
12
+ * "Cannot read properties of undefined (reading 'code')".
13
+ *
14
+ * The union object satisfies both readers, and {@link markdownTextProps}
15
+ * passes it under BOTH prop names — each host ignores the one it does not
16
+ * know.
17
+ *
18
+ * @module @huanlin/dsh-plugin-better-plan/client/markdown-props
19
+ */
20
+ import type { ComponentProps } from 'react';
21
+ import { MarkdownText } from '@deepseek-ai/dsh-client-ui-primitives';
22
+ /** The flat copy-button pair. */
23
+ export interface MarkdownCopyLabels {
24
+ copyLabel: string;
25
+ copiedLabel: string;
26
+ }
27
+ /** The dual-generation chrome labels object. */
28
+ export interface MarkdownChromeLabels extends MarkdownCopyLabels {
29
+ /** 0.1.2-alpha.1+ nested reads. */
30
+ code: MarkdownCopyLabels;
31
+ /** 0.1.2-alpha.1+ sr-only footnotes heading. */
32
+ footnotes: string;
33
+ }
34
+ /** Build the dual-shape chrome labels from a flat copy-button pair. */
35
+ export declare function markdownChromeLabels(labels: MarkdownCopyLabels): MarkdownChromeLabels;
36
+ /**
37
+ * MarkdownText props carrying the labels under BOTH prop names. The cast is
38
+ * load-bearing: this plugin builds against one generation's declaration where
39
+ * the other prop does not exist.
40
+ * @param text - the markdown source.
41
+ * @param labels - the flat copy-button pair.
42
+ * @returns props spreadable onto `MarkdownText`.
43
+ */
44
+ export declare function markdownTextProps(text: string, labels: MarkdownCopyLabels): ComponentProps<typeof MarkdownText>;
@@ -0,0 +1,76 @@
1
+ /**
2
+ * The client-side review state: a tiny external store fed by the delivery
3
+ * WebSocket's `{ kind: 'review', review }` frames and by the decision POST's
4
+ * echoed response, consumed by the Plan tab's action bar through
5
+ * `useSyncExternalStore`.
6
+ *
7
+ * The state mirrors the host gate's per-session review; a session switch
8
+ * resets it (the reconnect's attach replay restores the current state).
9
+ *
10
+ * @module @huanlin/dsh-plugin-better-plan/client/review-store
11
+ */
12
+ /** Mirrors the host gate's ReviewState (the wire face of a review frame). */
13
+ export interface ReviewState {
14
+ id: string;
15
+ path: string;
16
+ title: string;
17
+ status: 'pending' | 'approved' | 'delegated' | 'kept' | 'cancelled';
18
+ }
19
+ /** The server→view frame carrying review state. */
20
+ export interface ReviewFrame {
21
+ kind: 'review';
22
+ review: ReviewState | null;
23
+ }
24
+ /** The sidebar decision vocabulary the panel posts (mirrors the gate's). */
25
+ export type ReviewDecision = 'approve' | 'keep' | 'approve_new_session';
26
+ /** Whether an unknown wire value is a well-formed review state. */
27
+ export declare function isReviewState(value: unknown): value is ReviewState;
28
+ type Listener = () => void;
29
+ /** External store for the current session's review state. */
30
+ export declare class ReviewStore {
31
+ private state;
32
+ private listeners;
33
+ /** The current review state (null = nothing to review). */
34
+ get: () => ReviewState | null;
35
+ /** Replace the state and notify subscribers. */
36
+ set: (next: ReviewState | null) => void;
37
+ /** Clear the state (session switch); attach replay restores it. */
38
+ reset: () => void;
39
+ /** @returns the unsubscribe disposer. */
40
+ subscribe: (listener: Listener) => (() => void);
41
+ }
42
+ /** The singleton store the WS handler writes and the Plan tab reads. */
43
+ export declare const reviewStore: ReviewStore;
44
+ /** The exact pathname of the host's review decision route. */
45
+ export declare const REVIEW_API_PATH = "/better-plan/api/review";
46
+ /** The outcome of one review decision POST. */
47
+ export type ReviewDecisionOutcome = {
48
+ ok: true;
49
+ review: ReviewState;
50
+ } | {
51
+ ok: false;
52
+ error: string;
53
+ };
54
+ /**
55
+ * Bootstrap the review state over HTTP (the WS attach replay remains the
56
+ * live channel): fills the bar when a review frame was missed (stale bundle,
57
+ * reconnect gap). A live frame already in the store wins — the GET result is
58
+ * only applied while the store is empty, so a late null response can never
59
+ * clear a pending bar. The request reports the view's active locale so the
60
+ * host's user-facing copy follows the browser.
61
+ * @param sessionId - the session whose review state to read.
62
+ */
63
+ export declare function fetchReviewState(sessionId: string): Promise<void>;
64
+ /**
65
+ * Post one review decision to the host route and, on success, echo the
66
+ * settled state into the store (the submitting view updates immediately;
67
+ * other views follow over the WebSocket). The body reports the view's active
68
+ * locale; failures map the route's stable error codes to localized copy.
69
+ * @param sessionId - the session whose plan is under review.
70
+ * @param decision - the user's choice.
71
+ * @param feedback - optional keep-planning feedback.
72
+ * @param reviewId - the reviewed delivery's id (stale-click guard).
73
+ * @returns the outcome; failures keep the pending bar up for a retry.
74
+ */
75
+ export declare function submitReviewDecision(sessionId: string, decision: ReviewDecision, feedback: string | undefined, reviewId: string): Promise<ReviewDecisionOutcome>;
76
+ export {};
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Config schema for the better-plan plugin (Schemastery, strict).
3
+ *
4
+ * The plugin injects no extra system-prompt section: the new delivery contract
5
+ * travels entirely inside the shadowed `exit_plan_mode` tool description, so
6
+ * the built-in `plan:policy` section stays verbatim and these two knobs are
7
+ * the whole configuration surface.
8
+ *
9
+ * @module @huanlin/dsh-plugin-better-plan/config
10
+ */
11
+ import type { LocaleSetting } from './locale.ts';
12
+ /** Deployment-tunable configuration for the better-plan plugin. */
13
+ export interface BetterPlanConfig {
14
+ /**
15
+ * Plan-directory suggestion interpolated into the shadow tool's description
16
+ * (the example path the model is told to write plans under).
17
+ */
18
+ planDir: string;
19
+ /**
20
+ * Read cap of one plan file (bytes). A larger file is refused with guidance
21
+ * to trim the plan, so an unbounded write cannot flood the review channel.
22
+ */
23
+ maxPlanBytes: number;
24
+ /**
25
+ * Locale of the user-facing copy the host generates (the delivery render
26
+ * text, the steer messages, the no-sidebar review question). `auto`
27
+ * follows the connected sidebar view's reported locale (the browser's
28
+ * active DSH locale) and falls back to English; `zh` / `en` force one.
29
+ * Model-contract text (tool description, prompt rewrite, execute errors)
30
+ * stays English regardless.
31
+ */
32
+ locale: LocaleSetting;
33
+ }
34
+ /** Schemastery schema validated by the cordis Loader. */
35
+ export declare const Config: import("C:/Users/Administrator/.dsh/source/current/vendor/schemastery/lib/types/index").default<Schemastery.ObjectS<{
36
+ planDir: import("C:/Users/Administrator/.dsh/source/current/vendor/schemastery/lib/types/index").default<string, string>;
37
+ maxPlanBytes: import("C:/Users/Administrator/.dsh/source/current/vendor/schemastery/lib/types/index").default<number, number>;
38
+ locale: import("C:/Users/Administrator/.dsh/source/current/vendor/schemastery/lib/types/index").default<"zh" | "en" | "auto", "zh" | "en" | "auto">;
39
+ }>, Schemastery.ObjectT<{
40
+ planDir: import("C:/Users/Administrator/.dsh/source/current/vendor/schemastery/lib/types/index").default<string, string>;
41
+ maxPlanBytes: import("C:/Users/Administrator/.dsh/source/current/vendor/schemastery/lib/types/index").default<number, number>;
42
+ locale: import("C:/Users/Administrator/.dsh/source/current/vendor/schemastery/lib/types/index").default<"zh" | "en" | "auto", "zh" | "en" | "auto">;
43
+ }>>;
44
+ /**
45
+ * Resolve a raw config patch through the schema, returning a full
46
+ * {@link BetterPlanConfig} with defaults applied. Unknown keys are rejected
47
+ * here so a mistyped cordis.yml row fails loud at load instead of being
48
+ * silently ignored.
49
+ * @param input - a partial or complete config object.
50
+ * @returns the schema-resolved config.
51
+ * @throws when the input carries an unknown key or a value the schema rejects.
52
+ */
53
+ export declare function resolveBetterPlanConfig(input: Partial<BetterPlanConfig>): BetterPlanConfig;
@@ -0,0 +1,80 @@
1
+ /**
2
+ * The Context face this plugin's host half consumes: the vendored cordis
3
+ * `Context` intersected with structural mirrors of the services it touches.
4
+ *
5
+ * Intersection (not `declare module` augmentation) mirrors the sidebar's
6
+ * approach: DSH's own packages already augment `@deepseek-ai/cordis`, and
7
+ * restating members structurally avoids TS2717 merge conflicts with any
8
+ * other plugin's augmentation. The augmentations that DO reach this program
9
+ * through workspace types (the typed `agent/session-start` /
10
+ * `agent/pre-step` events from `@deepseek-ai/dsh-agent`) stay untouched.
11
+ *
12
+ * @module @huanlin/dsh-plugin-better-plan/context
13
+ */
14
+ import type { Context as CordisContext } from '@deepseek-ai/cordis';
15
+ import type { PersistenceStat } from './resolve-cwd.ts';
16
+ /** The upgrade route face this plugin registers on the host webServer. */
17
+ export interface BetterPlanUpgradeRoute {
18
+ path: string;
19
+ handler: (req: {
20
+ url?: string;
21
+ headers: Record<string, string | string[] | undefined>;
22
+ }, socket: {
23
+ destroy(): void;
24
+ }, head: Uint8Array) => void | Promise<void>;
25
+ }
26
+ /** The exact HTTP route face this plugin registers on the host webServer. */
27
+ export interface BetterPlanHttpRoute {
28
+ kind: 'exact';
29
+ path: string;
30
+ handler: (req: never, res: never) => void | Promise<void>;
31
+ }
32
+ /** The webServer service face this plugin uses. */
33
+ export interface BetterPlanWebServer {
34
+ registerUpgrade(route: BetterPlanUpgradeRoute): () => void;
35
+ register(route: BetterPlanHttpRoute): () => void;
36
+ }
37
+ /** The web runtime trust list (bind-derived; absent on non-web hosts). */
38
+ export interface BetterPlanWebRuntime {
39
+ trustedHosts: readonly string[];
40
+ }
41
+ /** The user-questions service face (structural mirror of the ask seam). */
42
+ export interface BetterPlanUserQuestions {
43
+ ask(request: {
44
+ questions: {
45
+ id: string;
46
+ question: string;
47
+ detail?: string;
48
+ header?: string;
49
+ options?: {
50
+ label: string;
51
+ description?: string;
52
+ }[];
53
+ intent?: {
54
+ kind: 'plan-review';
55
+ approve: string;
56
+ };
57
+ }[];
58
+ agent?: unknown;
59
+ signal?: AbortSignal;
60
+ }): Promise<{
61
+ answers: {
62
+ id: string;
63
+ selected: string[];
64
+ custom?: string;
65
+ }[];
66
+ }>;
67
+ }
68
+ /** The shape this plugin consumes. */
69
+ export interface BetterPlanContextShape {
70
+ /** The host webserver (upgrade routes). */
71
+ webServer: BetterPlanWebServer;
72
+ /** The web runtime trust list (optional: non-web hosts have none). */
73
+ webRuntime?: BetterPlanWebRuntime;
74
+ /** The user-questions seam (optional: review degrades without it). */
75
+ userQuestions?: BetterPlanUserQuestions;
76
+ /** The session-persistence service (optional: cwd fallback source). */
77
+ sessionPersistence?: PersistenceStat;
78
+ }
79
+ /** The Context this plugin's host half sees. */
80
+ export type Context = CordisContext & BetterPlanContextShape;
@@ -0,0 +1,59 @@
1
+ /**
2
+ * The plan-delivery registry: a per-session queue of plan pushes plus the
3
+ * connected sidebar views, following the sidebar's AgentOpenRegistry pattern
4
+ * (consume-on-send) with one addition — a bounded queue.
5
+ *
6
+ * `enqueue` adds a delivery and, when at least one view for the session is
7
+ * attached, pushes it immediately and clears the queue (`delivered: true`).
8
+ * With no attached view the delivery stays queued and `attach` replays it on
9
+ * connect. The queue holds at most {@link DELIVERY_QUEUE_LIMIT} entries per
10
+ * session (oldest dropped first): a session delivered while no browser is
11
+ * open must not accumulate unbounded plan pushes that all replay at once on
12
+ * the next attach — the newest plan is the one under review.
13
+ *
14
+ * @module @huanlin/dsh-plugin-better-plan/delivery-registry
15
+ */
16
+ /** One plan push (the wire face over the delivery WebSocket). */
17
+ export interface PlanDelivery {
18
+ /** Opaque id (host-generated; the client uses it only for dedupe/debug). */
19
+ id: string;
20
+ /** Absolute path of the plan file. */
21
+ path: string;
22
+ /** Sidebar tab title: the plan's first heading, else the file basename. */
23
+ title: string;
24
+ }
25
+ /** Maximum queued deliveries per session (oldest dropped beyond this). */
26
+ export declare const DELIVERY_QUEUE_LIMIT = 8;
27
+ /** One subscribed sidebar view's sender. */
28
+ type Sender = (delivery: PlanDelivery) => void;
29
+ /**
30
+ * Per-session delivery queues plus the connected views.
31
+ */
32
+ export declare class PlanDeliveryRegistry {
33
+ private pending;
34
+ private subscribers;
35
+ /**
36
+ * Queue one delivery and push it immediately when a view is attached.
37
+ * @param sessionId - the session whose plan panel is targeted.
38
+ * @param path - absolute path of the plan file.
39
+ * @param title - sidebar tab title for the plan.
40
+ * @returns the delivery id and whether a connected view received it now.
41
+ */
42
+ enqueue(sessionId: string, path: string, title: string): {
43
+ id: string;
44
+ delivered: boolean;
45
+ };
46
+ /**
47
+ * Attach one sidebar view; queued deliveries replay immediately
48
+ * (consume-on-send: a reconnect must never re-show a plan already open).
49
+ * @param sessionId - the session the view displays.
50
+ * @param send - the push callback for this view.
51
+ * @returns the disposer detaching the view.
52
+ */
53
+ attach(sessionId: string, send: Sender): () => void;
54
+ /** Drop every queued delivery (kept for symmetry with the sidebar registry). */
55
+ drainAll(): void;
56
+ /** Drop every queue and subscriber (plugin teardown). */
57
+ dispose(): void;
58
+ }
59
+ export {};
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Plan-title extraction, shared by the delivery registry (sidebar tab title),
3
+ * the WS push payload, and the canonical tool value.
4
+ *
5
+ * `firstHeading` mirrors the built-in plan mode's regex so both tools name a
6
+ * plan the same way. D2 keeps validation loose: a plan without any heading is
7
+ * accepted and falls back to the file basename.
8
+ *
9
+ * @module @huanlin/dsh-plugin-better-plan/first-heading
10
+ */
11
+ /**
12
+ * The plan's first markdown heading (any level), or `undefined` when the plan
13
+ * has none. Tolerates leading whitespace before `#` and trailing whitespace
14
+ * after the text; a 7-`#` run is not a heading.
15
+ * @param plan - the full plan markdown.
16
+ * @returns the heading text, or `undefined` when no line matches.
17
+ */
18
+ export declare function firstHeading(plan: string): string | undefined;
19
+ /**
20
+ * The last path segment of a POSIX or Windows path (mirror of the sidebar
21
+ * client's FileTree baseName), used as the fallback plan title.
22
+ * @param path - the path to shorten.
23
+ * @returns the basename without trailing separators.
24
+ */
25
+ export declare function basenameOf(path: string): string;