@guillaumemeyer/dsh-plan-approval 0.1.10 → 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 +30 -25
  2. package/client.js +37 -63
  3. package/package.json +1 -1
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
@@ -13,14 +13,16 @@ review contract.
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
 
@@ -73,8 +75,8 @@ After restart, the package's browser bundle is served under
73
75
  3. **Review the plan**:
74
76
  - **Navigate**: `j`/`k` move the cursor line, `gg` jump to top, `G` (Shift+g) jump to
75
77
  bottom; the plan auto-scrolls to follow the cursor.
76
- - **Search**: `/` opens the search bar — type to highlight matches, `Enter`/`n` to
77
- the next match, `N` (Shift+n) to the previous, `Esc` to close.
78
+ - **Search**: `/` opens the search bar — type to highlight matches, `Enter` to the
79
+ next match, `Shift+Enter` to the previous, `Esc` to close.
78
80
  - **Comment**: `c` (or **click** any line) anchors an inline comment to that line;
79
81
  type it and press `Enter` to add (`Esc` to cancel). Comments show as inline chips.
80
82
  - **Copy**: `y` copies the plan, including any inline comments (the merged plan).
@@ -99,7 +101,7 @@ y copy · q quit · j/k move · gg/G ends · / search`.
99
101
  | `j` / `k` | Move the cursor down / up one line |
100
102
  | `gg` | Jump to top |
101
103
  | `G` (Shift+g) | Jump to bottom |
102
- | `/` | Open search (type to highlight; `Enter`/`n` next, `N` prev, `Esc` close) |
104
+ | `/` | Open search (type to highlight; `Enter` next, `Shift+Enter` prev, `Esc` close) |
103
105
  | `Esc` | Close the search/comment box, otherwise quit |
104
106
 
105
107
  ## How the review answers map
@@ -107,22 +109,25 @@ y copy · q quit · j/k move · gg/G ends · / search`.
107
109
  The underlying `exit_plan_mode` review is binary (approve vs. keep-planning). This
108
110
  plugin keeps that contract and layers comments onto it:
109
111
 
110
- - **`s` (request changes)** sends the typed feedback **plus** the inline comments as
111
- the `custom` answer the host keeps planning, and the agent revises the plan with
112
- your comments. fully works.
113
- - **`a` (approve)** with **no** comments sends the plain approve answer.
114
- - **`a` (approve)** with **comments** sends `{ selected: [approve], custom: <comments> }`
115
- the **approve-with-notes** path. For this to be treated as an approval (rather than
116
- "keep planning"), the host's `exit_plan_mode` must accept `custom` on an approval.
117
- See [Host change](#host-change-approve-with-notes).
118
-
119
- > **Fallback if the host is not patched:** `a` with comments is delivered as feedback
120
- > (the host keeps planning), so nothing is silently dropped.
121
-
122
- ## Host change: approve-with-notes
123
-
124
- The shipped `@deepseek-ai/dsh-plan-mode` tool treats any `custom` value as "keep
125
- 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:
126
131
 
127
132
  ```js
128
133
  // before: if (... || item.custom !== void 0) { /* keep planning */ }
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.10",
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",