@guillaumemeyer/dsh-plan-approval 0.1.9 → 0.1.11

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.
Files changed (3) hide show
  1. package/README.md +122 -47
  2. package/client.js +37 -63
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
- # @guillaumemeyer/dsh-plan-approval
1
+ # 🐋 DeepSeek Harness Enhanced Plan Mode
2
2
 
3
- A fullscreen, Grok-Build-style **plan-review overlay** for DeepSeek Harness (DSH).
3
+ A fullscreen, **enhanced plan mode** for DeepSeek Harness (DSH).
4
4
 
5
5
  It replaces the small in-chat plan review card with a full-screen review surface so
6
6
  large plans are readable and navigable. It renders automatically when the agent
@@ -9,67 +9,125 @@ review contract.
9
9
 
10
10
  ## Features
11
11
 
12
- - **Fullscreen review** in the frame-wide `shell.overlay` layer, now at **90vw × 90vh**.
12
+ - **Fullscreen review** in the frame-wide `shell.overlay` layer, at **90vw × 90vh**.
13
13
  - **Line navigation** (vim-style): `j`/`k` move line-by-line, `gg` jump to top,
14
14
  `G` (Shift+g) jump to bottom, with a straight left highlight on the cursor line
15
15
  that auto-scrolls.
16
- - **Inline search**: `/` opens a search bar; matching lines highlight live, `Enter`/`n`
17
- cycle to the next match, `N` to the previous, `Esc` closes.
16
+ - **Inline search**: `/` opens a search bar; matching lines highlight live, `Enter` cycles
17
+ to the next match and `Shift+Enter` to the previous, `Esc` closes (`n`/`N` also work once
18
+ the overlay re-takes focus).
18
19
  - **Inline comments**: `c` (or **clicking a line**) drops a comment anchored to that
19
20
  line, shown as an inline chip. Comments are **integrated into the plan**:
20
21
  - On **request changes (`s`)** they're serialized into the feedback answer so the
21
22
  agent revises the plan with them.
22
- - On **approval (`a`)**, if comments exist they're attached to the approval (the
23
- approve-with-notes path; see below), and the merged plan is what's copied with `y`.
23
+ - On **approval (`a`)** the plan is approved and plan mode exits; the merged plan
24
+ (comments included) is what's copied with `y`. Comments are not attached to an
25
+ approval — use `s` (request changes) to send them back for revision.
24
26
  - **Actions**: `a` approve · `s` request changes · `y` copy plan · `q` quit plan.
25
27
  - A bottom **status line** lists every keyboard shortcut.
26
28
 
27
- ## Install
28
-
29
- Install the package into the DSH deployment:
30
-
31
- ```sh
32
- npm i @guillaumemeyer/dsh-plan-approval
33
- ```
34
-
35
- Then add one row to the **web surface (browser roster)** composition — e.g. the
36
- deployment's `cordis.patch.yml` / web `cordis.yml`, alongside other `dsh.client`
37
- browser roster rows:
38
-
39
- ```yaml
40
- - id: ui-plan-approval
41
- name: '@guillaumemeyer/dsh-plan-approval'
42
- ```
43
-
44
- Restart the DSH host. The modules node half re-scans loader entries, bundles
45
- `/plugins/@guillaumemeyer/dsh-plan-approval/client.js`, and the overlay
46
- appears on the next plan review.
47
-
48
- > The row must live in the **web-surface `dsh.client` roster** (a `dsh.client`
49
- > platform-`web` package). Placing it in an agent preset or a non-web composition
50
- > will not be scanned into `window.__DSH_BOOT__`.
29
+ ## Install (into a DSH deployment)
30
+
31
+ This is a `dsh.client` browser plugin, so it must be installed and mounted where DSH
32
+ composes its **web surface** (host/web composition), then the host restarted so the
33
+ client-modules node half produces the bundle.
34
+
35
+ 1. **Install the package** into the DSH deployment's web app:
36
+
37
+ ```sh
38
+ npm i @guillaumemeyer/dsh-plan-approval
39
+ ```
40
+
41
+ 2. **Find the web-surface (browser roster) composition.** This is the `cordis.yml` /
42
+ `cordis.patch.yml` in the DSH web app that lists the other browser roster rows
43
+ (`ui-plan`, `ui-user-questions`, `ui-tool`, …). (The shipped `@deepseek-ai/dsh-web-app`
44
+ `cordis.patch.yml` is the reference for this "browser plugin roster" section.)
45
+
46
+ 3. **Add one row** to that roster:
47
+
48
+ ```yaml
49
+ - id: ui-plan-approval
50
+ name: '@guillaumemeyer/dsh-plan-approval'
51
+ ```
52
+
53
+ 4. **Restart the DSH host.** The `@deepseek-ai/dsh-client-modules` node half re-scans
54
+ loader entries for packages declaring `dsh.client`, bundles
55
+ `/plugins/@guillaumemeyer/dsh-plan-approval/client.js`, and the browser loads it.
56
+ (With a `pnpm run dev:web` watcher, the bundle is rebuilt automatically without a
57
+ restart; in a production/built deployment a host restart is required.)
58
+
59
+ > The row **must** live in the **web-surface `dsh.client` roster** (a
60
+ > `dsh.client` `platform: "web"` package). Placing it in an agent preset or a
61
+ > non-web composition will **not** be scanned into `window.__DSH_BOOT__`.
62
+
63
+ ### Verify it's mounted
64
+
65
+ After restart, the package's browser bundle is served under
66
+ `/plugins/@guillaumemeyer/dsh-plan-approval/client.js`, and the plugin appears in the
67
+ `shell.overlay` slot.
68
+
69
+ ## Use it from DSH
70
+
71
+ 1. **Enter plan mode** — switch the agent into plan mode (the `/plan` command or the
72
+ session's plan control) and ask it to plan.
73
+ 2. **Present a plan** — when the agent has the plan, it calls `exit_plan_mode` with the
74
+ plan markdown. The full-screen review overlay opens automatically.
75
+ 3. **Review the plan**:
76
+ - **Navigate**: `j`/`k` move the cursor line, `gg` jump to top, `G` (Shift+g) jump to
77
+ bottom; the plan auto-scrolls to follow the cursor.
78
+ - **Search**: `/` opens the search bar — type to highlight matches, `Enter` to the
79
+ next match, `Shift+Enter` to the previous, `Esc` to close.
80
+ - **Comment**: `c` (or **click** any line) anchors an inline comment to that line;
81
+ type it and press `Enter` to add (`Esc` to cancel). Comments show as inline chips.
82
+ - **Copy**: `y` copies the plan, including any inline comments (the merged plan).
83
+ 4. **Decide**:
84
+ - `a` **approve** the plan.
85
+ - `s` **request changes** — the feedback box opens; submit your requested changes
86
+ (plus any inline comments) → the agent keeps planning and revises the plan.
87
+ - `q` **quit plan** — cancel the review and go talk instead.
88
+
89
+ The status line at the bottom shows every shortcut: `a approve · s changes · c comment ·
90
+ y copy · q quit · j/k move · gg/G ends · / search`.
91
+
92
+ ### Keyboard shortcuts
93
+
94
+ | Key | Action |
95
+ | --- | --- |
96
+ | `a` | Approve the plan |
97
+ | `s` | Request changes (opens feedback box; `Enter` submits) |
98
+ | `c` | Add an inline comment at the current line (`Enter` add, `Esc` cancel) |
99
+ | `y` | Copy the plan (merged with comments) |
100
+ | `q` | Quit / cancel the plan review |
101
+ | `j` / `k` | Move the cursor down / up one line |
102
+ | `gg` | Jump to top |
103
+ | `G` (Shift+g) | Jump to bottom |
104
+ | `/` | Open search (type to highlight; `Enter` next, `Shift+Enter` prev, `Esc` close) |
105
+ | `Esc` | Close the search/comment box, otherwise quit |
51
106
 
52
107
  ## How the review answers map
53
108
 
54
109
  The underlying `exit_plan_mode` review is binary (approve vs. keep-planning). This
55
110
  plugin keeps that contract and layers comments onto it:
56
111
 
57
- - **`s` (request changes)** sends the typed feedback **plus** the inline comments as
58
- the `custom` answer the host keeps planning, and the agent revises the plan with
59
- your comments. fully works.
60
- - **`a` (approve)** with **no** comments sends the plain approve answer.
61
- - **`a` (approve)** with **comments** sends `{ selected: [approve], custom: <comments> }`
62
- the **approve-with-notes** path. For this to be treated as an approval (rather than
63
- "keep planning"), the host's `exit_plan_mode` must accept `custom` on an approval.
64
- See [Host change](#host-change-approve-with-notes).
65
-
66
- > **Fallback if the host is not patched:** `a` with comments is delivered as feedback
67
- > (the host keeps planning), so nothing is silently dropped.
68
-
69
- ## Host change: approve-with-notes
70
-
71
- The shipped `@deepseek-ai/dsh-plan-mode` tool treats any `custom` value as "keep
72
- planning". To integrate comments on approval, patch its `exit_plan_mode`:
112
+ - **`a` (approve)** always sends the plain approve answer (`{ selected: [approve] }`),
113
+ so the host leaves plan mode and the plan is carried out from the next step whether
114
+ or not there are inline comments. Attaching the comments as `custom` on an approval
115
+ would make an unpatched host treat the approval as "keep planning" (keeping the
116
+ session in plan mode instead of switching to execution), so the approve path
117
+ deliberately keeps the answer clean.
118
+ - **`s` (request changes)** sends the typed feedback **plus** the inline comments as the
119
+ `custom` answer → the host keeps planning, and the agent revises the plan with your
120
+ comments. This is the path to use when you want your comments considered.
121
+ - The merged plan (plan + inline comments) is still available to copy with `y`.
122
+
123
+ ## Host change: approve-with-notes (optional)
124
+
125
+ The shipped `@deepseek-ai/dsh-plan-mode` tool treats any `custom` value on the review
126
+ answer as "keep planning". The plugin's approve path keeps that contract by sending a
127
+ plain approval, so an approval always leaves plan mode and starts execution. If you
128
+ additionally want inline comments auto-attached to an approval, re-enable the
129
+ `custom` field in `approve()` (`client.js`) and patch the host's `exit_plan_mode` to
130
+ accept it:
73
131
 
74
132
  ```js
75
133
  // before: if (... || item.custom !== void 0) { /* keep planning */ }
@@ -84,6 +142,23 @@ plus include it in the result `render` so the model sees the comments after appr
84
142
  This requires a host restart to take effect, and is a change to a shipped package that
85
143
  an upgrade would overwrite — ideally upstream it.
86
144
 
145
+ ## Publishing updates
146
+
147
+ To publish a new version, ensure a local `.env` (untracked) holds your npm token, then:
148
+
149
+ ```sh
150
+ cp .env.example .env # once: edit to add NPM_TOKEN=...
151
+ npm run release # runs publish:check (preversion), bumps patch, publishes to npm
152
+ ```
153
+
154
+ - The token is read from `.env` and referenced by the committed `.npmrc` via `${NPM_TOKEN}`,
155
+ so it is never tracked.
156
+ - `preversion` runs `node --check client.js && npm pack --dry-run` automatically before
157
+ the version bump.
158
+ - The script is named `release` because `publish` is a reserved npm lifecycle name: npm
159
+ auto-runs a `publish` script during `npm publish`. Naming it `publish` made `npm publish`
160
+ re-run the script, bumping and publishing a version on every loop.
161
+
87
162
  ## Notes
88
163
 
89
164
  - Client-only browser plugin; the node half (`index.js`) is an empty `apply`.
package/client.js CHANGED
@@ -12,6 +12,7 @@ window.__ModuleLoader__.load({
12
12
  .prv-title{font-size:17px;font-weight:600;margin:0;line-height:1.3;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
13
13
  .prv-search{display:flex;align-items:center;gap:10px;padding:8px 22px;border-bottom:1px solid var(--dsw-alias-border-l1);flex-shrink:0;}
14
14
  .prv-search-input{flex:1 1 auto;background:var(--dsw-alias-bg-overlay);color:var(--dsw-alias-label-primary);border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:6px 10px;font-size:13px;line-height:18px;}
15
+ .prv-search-input::placeholder{color:var(--dsw-alias-label-secondary);opacity:1;}
15
16
  .prv-search-count{font-size:12px;color:var(--dsw-alias-label-secondary);white-space:nowrap;}
16
17
  .prv-body{flex:1 1 auto;min-height:0;overflow-y:auto;padding:18px 26px 26px;overscroll-behavior:contain;position:relative;}
17
18
  .prv-line{font-size:14px;line-height:1.7;padding:0 6px;border-left:2px solid transparent;}
@@ -30,6 +31,7 @@ window.__ModuleLoader__.load({
30
31
  .prv-comment-field{margin:6px 0 2px;display:block;}
31
32
  .prv-field{flex-shrink:0;padding:12px 22px;border-top:1px solid var(--dsw-alias-border-l1);}
32
33
  .prv-input{width:100%;box-sizing:border-box;background:var(--dsw-alias-bg-overlay);color:var(--dsw-alias-label-primary);border:1px solid var(--dsw-alias-border-l2);border-radius:10px;padding:10px 12px;font-size:14px;line-height:1.5;resize:vertical;min-height:64px;}
34
+ .prv-input::placeholder{color:var(--dsw-alias-label-secondary);opacity:1;}
33
35
  .prv-fieldActions{display:flex;gap:8px;justify-content:flex-end;margin-top:8px;}
34
36
  .prv-footer{flex-shrink:0;display:flex;align-items:center;gap:14px;padding:12px 22px;border-top:1px solid var(--dsw-alias-border-l1);flex-wrap:wrap;}
35
37
  .prv-status{font-size:12px;line-height:16px;color:var(--dsw-alias-label-secondary);min-width:150px;}
@@ -54,46 +56,6 @@ window.__ModuleLoader__.load({
54
56
  document.head.appendChild(tag);
55
57
  }
56
58
 
57
- const OVERLAY_CSS = `.prv-root{position:fixed;inset:0;z-index:2147480000;background:rgba(8,10,16,0.62);display:flex;align-items:center;justify-content:center;pointer-events:auto;font-family:inherit;}
58
- .prv-panel{width:90vw;height:90vh;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);border:1px solid var(--dsw-alias-border-l2);border-radius:18px;box-shadow:0 36px 90px rgba(0,0,0,0.55);display:flex;flex-direction:column;overflow:hidden;}
59
- .prv-header{display:flex;align-items:center;gap:12px;padding:16px 22px;border-bottom:1px solid var(--dsw-alias-border-l1);flex-shrink:0;}
60
- .prv-badge{background:var(--dsw-alias-state-warn-primary);color:var(--dsw-alias-bg-base);font-size:11px;font-weight:600;letter-spacing:.05em;text-transform:uppercase;padding:3px 9px;border-radius:999px;}
61
- .prv-title{font-size:17px;font-weight:600;margin:0;line-height:1.3;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
62
- .prv-search{display:flex;align-items:center;gap:10px;padding:8px 22px;border-bottom:1px solid var(--dsw-alias-border-l1);flex-shrink:0;}
63
- .prv-search-input{flex:1 1 auto;background:var(--dsw-alias-bg-overlay);color:var(--dsw-alias-label-primary);border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:6px 10px;font-size:13px;line-height:18px;}
64
- .prv-search-count{font-size:12px;color:var(--dsw-alias-label-secondary);white-space:nowrap;}
65
- .prv-body{flex:1 1 auto;min-height:0;overflow-y:auto;padding:18px 26px 26px;overscroll-behavior:contain;position:relative;}
66
- .prv-line{font-size:14px;line-height:1.7;padding:0 6px;border-left:2px solid transparent;}
67
- .prv-line-current{background:var(--dsw-alias-bg-layer-2);border-left-color:var(--dsw-alias-brand-primary);}
68
- .prv-line-match{background:var(--dsw-alias-bg-overlay);}
69
- .prv-line-match-active{background:var(--dsw-alias-state-warn-primary);color:var(--dsw-alias-bg-base);}
70
- .prv-plain{white-space:pre-wrap;}
71
- .prv-h1{font-size:22px;font-weight:700;margin:8px 0 6px;line-height:1.3;}
72
- .prv-h2{font-size:18px;font-weight:600;margin:12px 0 4px;line-height:1.3;}
73
- .prv-h3{font-size:15px;font-weight:600;margin:10px 0 4px;line-height:1.3;}
74
- .prv-h4{font-size:14px;font-weight:600;margin:8px 0 4px;line-height:1.3;}
75
- .prv-li{font-size:14px;line-height:1.65;margin:0 0 4px;padding-left:16px;position:relative;}
76
- .prv-li:before{content:"•";position:absolute;left:6px;color:var(--dsw-alias-label-secondary);}
77
- .prv-blank{height:10px;}
78
- .prv-comment{display:inline-block;margin:0 4px;padding:1px 7px;border-radius:999px;background:var(--dsw-alias-bg-overlay);color:var(--dsw-alias-state-warn-primary);font-size:12px;line-height:16px;vertical-align:middle;white-space:normal;}
79
- .prv-comment-field{margin:6px 0 2px;display:block;}
80
- .prv-field{flex-shrink:0;padding:12px 22px;border-top:1px solid var(--dsw-alias-border-l1);}
81
- .prv-input{width:100%;box-sizing:border-box;background:var(--dsw-alias-bg-overlay);color:var(--dsw-alias-label-primary);border:1px solid var(--dsw-alias-border-l2);border-radius:10px;padding:10px 12px;font-size:14px;line-height:1.5;resize:vertical;min-height:64px;}
82
- .prv-fieldActions{display:flex;gap:8px;justify-content:flex-end;margin-top:8px;}
83
- .prv-footer{flex-shrink:0;display:flex;align-items:center;gap:14px;padding:12px 22px;border-top:1px solid var(--dsw-alias-border-l1);flex-wrap:wrap;}
84
- .prv-status{font-size:12px;line-height:16px;color:var(--dsw-alias-label-secondary);min-width:150px;}
85
- .prv-status.error{color:var(--dsw-alias-state-error-primary);}
86
- .prv-status.ok{color:var(--dsw-alias-state-success-primary);}
87
- .prv-hints{display:flex;align-items:center;gap:2px;flex-wrap:wrap;font-size:12px;color:var(--dsw-alias-label-secondary);}
88
- .prv-hints kbd{background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l2);border-radius:5px;padding:1px 6px;font-family:inherit;font-size:11px;font-weight:600;color:var(--dsw-alias-label-primary);margin:0 3px 0 8px;}
89
- .prv-actions{display:flex;gap:8px;margin-left:auto;}
90
- .prv-btn{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);border-radius:999px;padding:7px 14px;font-size:13px;font-weight:500;cursor:pointer;line-height:18px;}
91
- .prv-btn:hover:not(:disabled){background:var(--dsw-alias-bg-layer-1);}
92
- .prv-btn:disabled{opacity:.55;cursor:default;}
93
- .prv-btn.warn{border-color:var(--dsw-alias-state-warn-primary);color:var(--dsw-alias-state-warn-primary);}
94
- .prv-btn.primary{background:var(--dsw-alias-state-success-primary);border-color:var(--dsw-alias-state-success-primary);color:var(--dsw-alias-bg-base);}
95
- .prv-btn.ghost{border-color:transparent;background:transparent;color:var(--dsw-alias-label-secondary);}
96
- .prv-btn.ghost:hover:not(:disabled){background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);}`;
97
59
 
98
60
  function planReviewOf(questions) {
99
61
  if (!questions || questions.length !== 1) return void 0;
@@ -105,7 +67,7 @@ function planReviewOf(questions) {
105
67
  if (options.length > 2) return void 0;
106
68
  const approve = options.find((o) => o.label === intent.approve);
107
69
  if (approve === void 0) return void 0;
108
- return { id: question.id, question: question.question, plan: question.detail, approve: approve };
70
+ return { id: question.id, question: question.question, plan: question.detail, approveOption: approve };
109
71
  }
110
72
 
111
73
  function parsePlan(plan) {
@@ -127,11 +89,8 @@ const PLAN_HINTS = [
127
89
  ['j/k', 'move'], ['gg/G', 'ends'], ['/', 'search'],
128
90
  ];
129
91
 
130
- function PlanReviewOverlay(props) {
131
- const sessions = props.sessions;
132
- const current = props.useSessions((s) => s.current);
92
+ function usePlanSession(sessions, current) {
133
93
  const [snap, setSnap] = react.useState(null);
134
-
135
94
  react.useEffect(() => {
136
95
  if (!sessions || !current) { setSnap(null); return void 0; }
137
96
  const bind = sessions.binding(current);
@@ -141,6 +100,13 @@ function PlanReviewOverlay(props) {
141
100
  update();
142
101
  return face.subscribe(update);
143
102
  }, [sessions, current]);
103
+ return snap;
104
+ }
105
+
106
+ function PlanReviewOverlay(props) {
107
+ const sessions = props.sessions;
108
+ const current = props.useSessions((s) => s.current);
109
+ const snap = usePlanSession(sessions, current);
144
110
 
145
111
  const [mode, setMode] = react.useState(null);
146
112
  const [text, setText] = react.useState('');
@@ -178,7 +144,12 @@ function PlanReviewOverlay(props) {
178
144
  }
179
145
  }
180
146
  const reviewKey = wait ? wait.key : null;
181
- const planLines = review ? parsePlan(review.plan) : [];
147
+ const planLines = react.useMemo(() => (review ? parsePlan(review.plan) : []), [review ? review.plan : null]);
148
+ const commentsByLine = react.useMemo(() => {
149
+ const byLine = {};
150
+ for (const c of comments) { (byLine[c.line] || (byLine[c.line] = [])).push(c); }
151
+ return byLine;
152
+ }, [comments]);
182
153
 
183
154
  react.useEffect(() => {
184
155
  setMode(null); setText(''); setError(null); setNotice(null); setBusy(false);
@@ -198,24 +169,25 @@ function PlanReviewOverlay(props) {
198
169
 
199
170
  if (!review || !wait) return null;
200
171
 
201
- const submitAnswer = (payload) => {
202
- if (busy) return;
203
- setBusy(true); setError(null); setNotice(null);
204
- wait.respond({ ok: true, value: { sessionId: wait.sessionId, answer: payload } })
205
- .then((receipt) => { if (!receipt || !receipt.accepted) throw new Error('response rejected'); })
206
- .catch((cause) => { setBusy(false); setError(cause instanceof Error ? cause.message : String(cause)); });
207
- };
208
- const submitCancel = () => {
172
+ const send = (payload, rejectMessage = 'response rejected') => {
209
173
  if (busy) return;
210
174
  setBusy(true); setError(null); setNotice(null);
211
- wait.respond({ ok: false, error: { code: 'cancelled', message: 'the user closed this question request', details: {} } })
212
- .then((receipt) => { if (!receipt || !receipt.accepted) throw new Error('cancellation rejected'); })
175
+ wait.respond(payload)
176
+ .then((receipt) => { if (!receipt || !receipt.accepted) throw new Error(rejectMessage); })
213
177
  .catch((cause) => { setBusy(false); setError(cause instanceof Error ? cause.message : String(cause)); });
214
178
  };
179
+ const submitAnswer = (payload) => send(
180
+ { ok: true, value: { sessionId: wait.sessionId, answer: payload } }, 'response rejected');
181
+ const submitCancel = () => send(
182
+ { ok: false, error: { code: 'cancelled', message: 'the user closed this question request', details: {} } }, 'cancellation rejected');
215
183
 
216
184
  const approve = () => {
217
- if (comments.length === 0) { submitAnswer({ answers: [{ id: review.id, selected: [review.approve.label] }] }); return; }
218
- submitAnswer({ answers: [{ id: review.id, selected: [review.approve.label], custom: commentBlock() }] });
185
+ // Always send a plain approval. Attaching the comments as `custom` here would
186
+ // make an unpatched host (dsh-plan-mode) treat this as "keep planning",
187
+ // leaving the session in plan mode instead of switching to execution. Use
188
+ // `s` (request changes) to send comments back for revision; use `y` to copy
189
+ // the plan with comments for your own reference.
190
+ submitAnswer({ answers: [{ id: review.id, selected: [review.approveOption.label] }] });
219
191
  };
220
192
  const quit = () => submitCancel();
221
193
  const copyPlan = () => {
@@ -275,7 +247,8 @@ function PlanReviewOverlay(props) {
275
247
  setCursor(matches[prev]);
276
248
  };
277
249
  const onSearchKey = (e) => {
278
- if (e.key === 'Enter') { e.preventDefault(); searchNext(); }
250
+ if (e.key === 'Enter' && e.shiftKey) { e.preventDefault(); searchPrev(); }
251
+ else if (e.key === 'Enter') { e.preventDefault(); searchNext(); }
279
252
  else if (e.key === 'Escape') { e.preventDefault(); closeSearch(); }
280
253
  };
281
254
 
@@ -311,8 +284,8 @@ function PlanReviewOverlay(props) {
311
284
  if (busy) return;
312
285
  const rawKey = e.key || '';
313
286
  const key = rawKey.toLowerCase();
314
- if (rawKey === 'G') { e.preventDefault(); goBottom(); return; }
315
- if (rawKey === 'N') { e.preventDefault(); searchPrev(); return; }
287
+ if (rawKey === 'G') { e.preventDefault(); ggArmed.current = false; goBottom(); return; }
288
+ if (rawKey === 'N') { e.preventDefault(); ggArmed.current = false; searchPrev(); return; }
316
289
  if (key === 'g') {
317
290
  e.preventDefault();
318
291
  if (ggArmed.current) { ggArmed.current = false; goTop(); }
@@ -328,7 +301,8 @@ function PlanReviewOverlay(props) {
328
301
  else if (key === 'a') { e.preventDefault(); approve(); }
329
302
  else if (key === 's') { e.preventDefault(); openInput('changes'); }
330
303
  else if (key === 'y') { e.preventDefault(); copyPlan(); }
331
- else if (key === 'q' || key === 'escape') {
304
+ else if (key === 'q') { e.preventDefault(); quit(); }
305
+ else if (key === 'escape') {
332
306
  e.preventDefault();
333
307
  if (searchOpen) closeSearch();
334
308
  else if (commentDraft) cancelComment();
@@ -353,7 +327,7 @@ function PlanReviewOverlay(props) {
353
327
  (i === cursor ? ' prv-line-current' : '') +
354
328
  (matchSet.has(i) ? ' prv-line-match' : '') +
355
329
  (i === activeLine ? ' prv-line-match-active' : '');
356
- const lineComments = comments.filter((c) => c.line === i);
330
+ const lineComments = commentsByLine[i] || [];
357
331
  const isComposing = commentDraft && commentLine === i;
358
332
  const children = [];
359
333
  if (line.kind !== 'blank') children.push(line.text);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@guillaumemeyer/dsh-plan-approval",
3
- "version": "0.1.9",
3
+ "version": "0.1.11",
4
4
  "description": "Fullscreen, Grok-Build-style DSH plan-review overlay with vim navigation, inline search, inline comments, and comment integration into the plan.",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -32,7 +32,7 @@
32
32
  "access": "public"
33
33
  },
34
34
  "scripts": {
35
- "publish": "node scripts/publish.mjs",
35
+ "release": "node scripts/publish.mjs",
36
36
  "publish:dry": "npm pack --dry-run",
37
37
  "publish:check": "node --check client.js && npm pack --dry-run",
38
38
  "preversion": "npm run publish:check"