@hienlh/ppm 0.9.86 → 0.9.88

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 (52) hide show
  1. package/260415-0932-git-graph-stash-rebase-conflicts/reports/code-reviewer-260415-1020-stash-rebase-conflicts.md +288 -0
  2. package/260415-0932-git-graph-stash-rebase-conflicts/reports/tester-260415-1020-build-check.md +117 -0
  3. package/260415-1150-ext-silent-failure-debugging/reports/code-reviewer-260415-1159-ext-error-reporting-review.md +205 -0
  4. package/260415-1150-ext-silent-failure-debugging/reports/docs-manager-260415-1206-ext-error-reporting.md +99 -0
  5. package/260415-1150-ext-silent-failure-debugging/reports/tester-260415-1159-extension-error-reporting.md +174 -0
  6. package/CHANGELOG.md +19 -0
  7. package/dist/web/assets/{chat-tab-BEEd-Km4.js → chat-tab-R4gKsnxD.js} +1 -1
  8. package/dist/web/assets/{code-editor-Ij4p30cr.js → code-editor-Br0vzTOy.js} +2 -2
  9. package/dist/web/assets/conflict-editor-BPgCjnNz.js +19 -0
  10. package/dist/web/assets/{csv-preview-CwQnOa3E.js → csv-preview-BZRICDP0.js} +1 -1
  11. package/dist/web/assets/{database-viewer-C1UHSgft.js → database-viewer-DaUoQ-oR.js} +1 -1
  12. package/dist/web/assets/{diff-viewer-CVx5naBA.js → diff-viewer-BzvK3gAE.js} +1 -1
  13. package/dist/web/assets/extension-webview-CGepEw-b.js +3 -0
  14. package/dist/web/assets/{index-OqgGFmh8.js → index-CKsEzQ4f.js} +4 -4
  15. package/dist/web/assets/index-Chf0otez.css +2 -0
  16. package/dist/web/assets/keybindings-store-D5zgHod8.js +1 -0
  17. package/dist/web/assets/{markdown-renderer-CRy8xw2B.js → markdown-renderer-DSYnGywb.js} +1 -1
  18. package/dist/web/assets/{port-forwarding-tab-Biua8ov5.js → port-forwarding-tab-vmqDKmk2.js} +1 -1
  19. package/dist/web/assets/{postgres-viewer-BcVjCAl4.js → postgres-viewer-0lIAosrr.js} +1 -1
  20. package/dist/web/assets/{settings-tab-C9X-N8hE.js → settings-tab-CMnv1fce.js} +1 -1
  21. package/dist/web/assets/{sql-query-editor-BFvRvJn0.js → sql-query-editor-Bc2hAwqT.js} +1 -1
  22. package/dist/web/assets/{sqlite-viewer-CPfvwFl4.js → sqlite-viewer-B60MS2Dy.js} +1 -1
  23. package/dist/web/assets/{terminal-tab-mWwk_weB.js → terminal-tab-CCJoLstH.js} +1 -1
  24. package/dist/web/assets/{use-monaco-theme-CPaeSMAA.js → use-monaco-theme-BJK48EmK.js} +1 -1
  25. package/dist/web/index.html +2 -2
  26. package/dist/web/sw.js +1 -1
  27. package/docs/codebase-summary.md +39 -6
  28. package/docs/project-changelog.md +86 -25
  29. package/docs/project-roadmap.md +3 -2
  30. package/docs/system-architecture.md +44 -1
  31. package/package.json +1 -1
  32. package/packages/ext-git-graph/src/extension.ts +126 -5
  33. package/packages/ext-git-graph/src/types.ts +13 -2
  34. package/packages/ext-git-graph/src/webview-html.ts +249 -31
  35. package/src/server/ws/extensions.ts +28 -2
  36. package/src/services/extension-host-worker.ts +6 -1
  37. package/src/services/extension.service.ts +17 -3
  38. package/src/types/extension-messages.ts +1 -1
  39. package/src/web/components/editor/conflict-editor.tsx +368 -0
  40. package/src/web/components/extensions/extension-webview.tsx +45 -3
  41. package/src/web/components/layout/editor-panel.tsx +1 -0
  42. package/src/web/components/layout/mobile-nav.tsx +1 -0
  43. package/src/web/components/layout/tab-bar.tsx +1 -0
  44. package/src/web/components/layout/tab-content.tsx +5 -0
  45. package/src/web/hooks/use-extension-ws.ts +8 -0
  46. package/src/web/stores/extension-store.ts +8 -0
  47. package/src/web/stores/panel-utils.ts +2 -0
  48. package/src/web/stores/tab-store.ts +2 -1
  49. package/dist/web/assets/extension-webview-CHVVpV34.js +0 -3
  50. package/dist/web/assets/index-vA7juDri.css +0 -2
  51. package/dist/web/assets/keybindings-store-BQxgPV5o.js +0 -1
  52. /package/dist/web/assets/{lib-CeBVkQ-7.js → lib-DSLzfeW0.js} +0 -0
@@ -27,6 +27,16 @@ ${getStyles()}
27
27
  <button id="btn-fetch" title="Fetch from remotes"></button>
28
28
  </div>
29
29
  <div class="toolbar-right">
30
+ <div class="stash-dropdown">
31
+ <button id="btn-stash" title="Stashes"></button>
32
+ <div id="stash-popover" class="stash-popover hidden">
33
+ <div class="stash-popover-header"><span>Stashes</span></div>
34
+ <div id="stash-list" class="stash-list"></div>
35
+ <div class="stash-popover-footer">
36
+ <button id="stash-save" class="btn-sm">+ Stash Changes</button>
37
+ </div>
38
+ </div>
39
+ </div>
30
40
  <div class="worktree-dropdown">
31
41
  <button id="btn-worktree" title="Worktrees"></button>
32
42
  <div id="worktree-popover" class="worktree-popover hidden">
@@ -146,33 +156,33 @@ function getStyles(): string {
146
156
  --border: #27272a; --border2: #3f3f46; --selected: #1e293b; --surface-hover: #27272a;
147
157
  }
148
158
  }
149
- body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: var(--bg); color: var(--text); font-size: 13px; overflow: hidden; height: 100vh; display: flex; flex-direction: column; }
159
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: var(--bg); color: var(--text); font-size: 12px; overflow: hidden; height: 100vh; display: flex; flex-direction: column; }
150
160
  #app { display: flex; flex-direction: column; height: 100vh; }
151
161
 
152
162
  /* Toolbar */
153
- #toolbar { display: flex; justify-content: space-between; align-items: center; padding: 6px 12px; border-bottom: 1px solid var(--border); background: var(--surface); flex-shrink: 0; }
154
- .toolbar-left, .toolbar-right { display: flex; align-items: center; gap: 6px; }
155
- select { background: var(--bg); color: var(--text); border: 1px solid var(--border2); border-radius: 4px; padding: 4px 8px; font-size: 12px; }
156
- button { background: transparent; color: var(--text); border: 1px solid var(--border); border-radius: 6px; padding: 4px 10px; font-size: 12px; cursor: pointer; min-width: 28px; min-height: 28px; transition: background 0.15s, border-color 0.15s; }
163
+ #toolbar { display: flex; justify-content: space-between; align-items: center; padding: 4px 10px; border-bottom: 1px solid var(--border); background: var(--surface); flex-shrink: 0; }
164
+ .toolbar-left, .toolbar-right { display: flex; align-items: center; gap: 4px; }
165
+ select { background: var(--bg); color: var(--text); border: 1px solid var(--border2); border-radius: 4px; padding: 3px 6px; font-size: 11px; }
166
+ button { background: transparent; color: var(--text); border: 1px solid var(--border); border-radius: 5px; padding: 3px 8px; font-size: 11px; cursor: pointer; min-width: 24px; min-height: 24px; transition: background 0.15s, border-color 0.15s; }
157
167
  button:hover { background: var(--surface-hover); border-color: var(--border2); }
158
168
  button:active { background: var(--surface); }
159
169
  .btn-fetching { opacity: 0.6; pointer-events: none; }
160
170
 
161
171
  /* Branch dropdown */
162
172
  .branch-dropdown { position: relative; }
163
- .branch-trigger { background: var(--bg); color: var(--text); border: 1px solid var(--border); border-radius: 6px; padding: 4px 24px 4px 8px; font-size: 12px; cursor: pointer; min-width: 140px; text-align: left; position: relative; }
173
+ .branch-trigger { background: var(--bg); color: var(--text); border: 1px solid var(--border); border-radius: 5px; padding: 3px 22px 3px 7px; font-size: 11px; cursor: pointer; min-width: 120px; text-align: left; position: relative; }
164
174
  .branch-trigger::after { content: '\\25BC'; font-size: 8px; position: absolute; right: 8px; top: 50%; transform: translateY(-50%); color: var(--subtext); }
165
175
  .branch-dropdown-menu { position: absolute; top: 100%; left: 0; z-index: 60; background: var(--surface); border: 1px solid var(--border2); border-radius: 6px; box-shadow: 0 4px 12px rgba(0,0,0,0.15); min-width: 220px; max-height: 300px; display: flex; flex-direction: column; margin-top: 2px; }
166
- .branch-filter-input { padding: 6px 8px; border: none; border-bottom: 1px solid var(--border); background: var(--bg); color: var(--text); font-size: 12px; outline: none; border-radius: 6px 6px 0 0; }
176
+ .branch-filter-input { padding: 4px 7px; border: none; border-bottom: 1px solid var(--border); background: var(--bg); color: var(--text); font-size: 11px; outline: none; border-radius: 5px 5px 0 0; }
167
177
  .branch-list { overflow-y: auto; max-height: 250px; }
168
- .branch-option { padding: 6px 10px; cursor: pointer; font-size: 12px; display: flex; align-items: center; gap: 6px; }
178
+ .branch-option { padding: 4px 8px; cursor: pointer; font-size: 11px; display: flex; align-items: center; gap: 5px; }
169
179
  .branch-option:hover { background: var(--surface-hover); }
170
180
  .branch-option.selected { background: var(--selected); font-weight: 600; }
171
181
 
172
182
  /* Worktree popover */
173
183
  .worktree-dropdown { position: relative; }
174
- #btn-worktree { display: flex; align-items: center; gap: 4px; font-size: 12px; padding: 4px 8px; }
175
- #btn-worktree .wt-count { background: var(--accent, #58a6ff); color: #fff; font-size: 10px; border-radius: 8px; padding: 0 5px; min-width: 16px; text-align: center; line-height: 16px; }
184
+ #btn-worktree { display: flex; align-items: center; gap: 3px; font-size: 11px; padding: 3px 6px; }
185
+ #btn-worktree .wt-count { background: var(--accent, #58a6ff); color: #fff; font-size: 9px; border-radius: 7px; padding: 0 4px; min-width: 14px; text-align: center; line-height: 14px; }
176
186
  .worktree-popover { position: absolute; top: 100%; right: 0; z-index: 60; background: var(--surface); border: 1px solid var(--border2); border-radius: 8px; box-shadow: 0 4px 16px rgba(0,0,0,0.2); min-width: 300px; max-width: 400px; margin-top: 4px; display: flex; flex-direction: column; }
177
187
  .worktree-popover-header { padding: 8px 12px; font-size: 12px; font-weight: 600; border-bottom: 1px solid var(--border); }
178
188
  .worktree-list { overflow-y: auto; max-height: 240px; }
@@ -192,19 +202,50 @@ button:active { background: var(--surface); }
192
202
  .wt-empty { padding: 16px; text-align: center; font-size: 11px; color: var(--subtext); }
193
203
  @media (max-width: 768px) { .branch-option { padding: 10px 12px; min-height: 44px; } }
194
204
 
205
+ /* Stash popover */
206
+ .stash-dropdown { position: relative; }
207
+ #btn-stash { display: flex; align-items: center; gap: 3px; font-size: 11px; padding: 3px 6px; }
208
+ #btn-stash .stash-count { background: var(--accent, #58a6ff); color: #fff; font-size: 9px; border-radius: 7px; padding: 0 4px; min-width: 14px; text-align: center; line-height: 14px; }
209
+ .stash-popover { position: absolute; top: 100%; right: 0; z-index: 60; background: var(--surface); border: 1px solid var(--border2); border-radius: 8px; box-shadow: 0 4px 16px rgba(0,0,0,0.2); min-width: 300px; max-width: 400px; margin-top: 4px; display: flex; flex-direction: column; }
210
+ .stash-popover-header { padding: 8px 12px; font-size: 12px; font-weight: 600; border-bottom: 1px solid var(--border); }
211
+ .stash-list { overflow-y: auto; max-height: 240px; }
212
+ .stash-item { padding: 8px 12px; border-bottom: 1px solid var(--border); display: flex; align-items: center; gap: 8px; font-size: 12px; }
213
+ .stash-item:last-child { border-bottom: none; }
214
+ .stash-item-info { flex: 1; min-width: 0; }
215
+ .stash-item-ref { font-weight: 600; font-size: 11px; color: var(--subtext); }
216
+ .stash-item-msg { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
217
+ .stash-item-actions { display: flex; gap: 4px; flex-shrink: 0; }
218
+ .stash-item-actions button { min-width: 24px; min-height: 24px; padding: 2px 6px; font-size: 10px; }
219
+ .stash-empty { padding: 16px; text-align: center; font-size: 11px; color: var(--subtext); }
220
+ .stash-popover-footer { padding: 8px 12px; border-top: 1px solid var(--border); display: flex; gap: 6px; }
221
+ .stash-popover-footer .btn-sm { flex: 1; }
222
+
223
+ /* Merge/rebase banner */
224
+ .merge-banner { padding: 8px 12px; background: rgba(133, 77, 14, 0.12); border-bottom: 1px solid var(--border); display: flex; align-items: center; gap: 8px; font-size: 12px; flex-shrink: 0; }
225
+ .merge-banner .banner-icon { color: #eab308; }
226
+ .merge-banner .banner-text { flex: 1; }
227
+ .merge-banner .banner-actions { display: flex; gap: 4px; }
228
+ .merge-banner .banner-actions button { font-size: 11px; padding: 2px 8px; }
229
+ .merge-banner .btn-continue { background: var(--green); color: #fff; border-color: transparent; }
230
+ .merge-banner .btn-abort { background: var(--red); color: #fff; border-color: transparent; }
231
+
232
+ /* Conflict section */
233
+ .conflict-header { padding: 4px 0; font-size: 12px; font-weight: 600; color: var(--red); display: flex; align-items: center; gap: 4px; }
234
+ .file-status-U { color: var(--red); font-weight: bold; }
235
+
195
236
  /* Find bar */
196
- .find-bar { display: flex; align-items: center; gap: 6px; padding: 6px 12px; border-bottom: 1px solid var(--border); background: var(--surface); }
197
- .find-bar input { flex: 1; background: var(--bg); color: var(--text); border: 1px solid var(--border2); border-radius: 4px; padding: 4px 8px; font-size: 12px; }
237
+ .find-bar { display: flex; align-items: center; gap: 5px; padding: 4px 10px; border-bottom: 1px solid var(--border); background: var(--surface); }
238
+ .find-bar input { flex: 1; background: var(--bg); color: var(--text); border: 1px solid var(--border2); border-radius: 4px; padding: 3px 6px; font-size: 11px; }
198
239
  .find-bar input:focus { outline: none; border-color: var(--blue); }
199
- #find-count { font-size: 11px; color: var(--subtext); min-width: 60px; }
240
+ #find-count { font-size: 10px; color: var(--subtext); min-width: 50px; }
200
241
  .hidden { display: none !important; }
201
242
 
202
243
  /* Graph container */
203
244
  #graph-container { flex: 1; overflow-y: auto; overflow-x: hidden; }
204
- .commit-row { display: flex; align-items: center; cursor: pointer; min-height: 28px; padding: 0 8px; }
245
+ .commit-row { display: flex; align-items: center; cursor: pointer; min-height: 24px; padding: 0 6px; font-size: 12px; }
205
246
  .commit-row:hover { background: var(--surface-hover); }
206
247
  .commit-row.selected { background: var(--selected); }
207
- .commit-row.header-row { background: var(--surface); cursor: default; font-weight: 600; font-size: 11px; color: var(--subtext); text-transform: uppercase; letter-spacing: 0.5px; position: sticky; top: 0; z-index: 2; border-bottom: 1px solid var(--border); }
248
+ .commit-row.header-row { background: var(--surface); cursor: default; font-weight: 600; font-size: 10px; color: var(--subtext); text-transform: uppercase; letter-spacing: 0.5px; position: sticky; top: 0; z-index: 2; border-bottom: 1px solid var(--border); min-height: 22px; }
208
249
  .commit-row.search-match { background: rgba(234, 179, 8, 0.15); }
209
250
  .commit-row.virtual { opacity: 0.85; font-style: italic; }
210
251
  .commit-row.virtual .col-message { color: var(--subtext); }
@@ -213,13 +254,13 @@ button:active { background: var(--surface); }
213
254
  .col-graph { width: var(--graph-col-w, 120px); min-width: var(--graph-col-w, 80px); overflow: hidden; flex-shrink: 0; position: relative; }
214
255
  .graph-resize-handle { position: absolute; right: 0; top: 0; bottom: 0; width: 6px; cursor: col-resize; z-index: 3; background: transparent; }
215
256
  .graph-resize-handle:hover, .graph-resize-handle.dragging { background: var(--blue); opacity: 0.5; }
216
- .col-message { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; padding: 0 8px; }
217
- .col-author { width: 120px; min-width: 120px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--subtext); font-size: 12px; }
218
- .col-date { width: 100px; min-width: 100px; color: var(--subtext); font-size: 12px; }
219
- .col-hash { width: 70px; min-width: 70px; font-family: 'SF Mono', 'Fira Code', monospace; font-size: 11px; color: var(--subtle); }
257
+ .col-message { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; padding: 0 6px; }
258
+ .col-author { width: 100px; min-width: 100px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--subtext); font-size: 11px; }
259
+ .col-date { width: 80px; min-width: 80px; color: var(--subtext); font-size: 11px; }
260
+ .col-hash { width: 60px; min-width: 60px; font-family: 'SF Mono', 'Fira Code', monospace; font-size: 10px; color: var(--subtle); }
220
261
 
221
262
  /* Ref badges */
222
- .ref-badge { display: inline-block; padding: 1px 6px; border-radius: 3px; font-size: 10px; font-weight: 600; margin-right: 4px; vertical-align: middle; }
263
+ .ref-badge { display: inline-block; padding: 0px 5px; border-radius: 3px; font-size: 9px; font-weight: 600; margin-right: 3px; vertical-align: middle; line-height: 16px; }
223
264
  .ref-head { background: var(--green); color: #fff; }
224
265
  .ref-local { background: var(--blue); color: #fff; }
225
266
  .ref-remote { background: var(--purple); color: #fff; }
@@ -236,14 +277,14 @@ button:active { background: var(--surface); }
236
277
  .commit-row.graph-hover { background: var(--surface-hover); }
237
278
 
238
279
  /* Detail panel */
239
- .detail-panel { border-top: 1px solid var(--border2); background: var(--surface); max-height: 40vh; overflow-y: auto; padding: 12px 16px; flex-shrink: 0; }
240
- .detail-panel h3 { font-size: 14px; margin-bottom: 8px; }
241
- .detail-field { margin-bottom: 4px; font-size: 12px; }
280
+ .detail-panel { border-top: 1px solid var(--border2); background: var(--surface); max-height: 40vh; overflow-y: auto; padding: 8px 12px; flex-shrink: 0; }
281
+ .detail-panel h3 { font-size: 13px; margin-bottom: 6px; }
282
+ .detail-field { margin-bottom: 3px; font-size: 11px; }
242
283
  .detail-field .label { color: var(--subtext); display: inline-block; width: 80px; }
243
- .detail-message { background: var(--bg); border: 1px solid var(--border); border-radius: 4px; padding: 8px; margin: 8px 0; font-size: 12px; white-space: pre-wrap; font-family: 'SF Mono', 'Fira Code', monospace; }
284
+ .detail-message { background: var(--bg); border: 1px solid var(--border); border-radius: 4px; padding: 6px; margin: 6px 0; font-size: 11px; white-space: pre-wrap; font-family: 'SF Mono', 'Fira Code', monospace; }
244
285
  .file-list { margin-top: 8px; }
245
- .file-item { display: flex; align-items: center; gap: 6px; padding: 2px 0; font-size: 12px; font-family: 'SF Mono', 'Fira Code', monospace; }
246
- .file-status { display: inline-block; width: 16px; text-align: center; font-weight: 700; font-size: 11px; }
286
+ .file-item { display: flex; align-items: center; gap: 5px; padding: 1px 0; font-size: 11px; font-family: 'SF Mono', 'Fira Code', monospace; }
287
+ .file-status { display: inline-block; width: 14px; text-align: center; font-weight: 700; font-size: 10px; }
247
288
  .file-status-A { color: var(--green); }
248
289
  .file-status-M { color: var(--yellow); }
249
290
  .file-status-D { color: var(--red); }
@@ -393,6 +434,7 @@ const state = {
393
434
  graphColWidth: null,
394
435
  fileViewMode: 'list',
395
436
  worktrees: [],
437
+ mergeState: null,
396
438
  _lastDetail: null,
397
439
  };
398
440
 
@@ -412,6 +454,7 @@ const ICONS = {
412
454
  fileOpen: '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 13v6a2 2 0 01-2 2H5a2 2 0 01-2-2V8a2 2 0 012-2h6"/><polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/></svg>',
413
455
  gitBranch: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="6" y1="3" x2="6" y2="15"/><circle cx="18" cy="6" r="3"/><circle cx="6" cy="18" r="3"/><path d="M18 9a9 9 0 01-9 9"/></svg>',
414
456
  trash: '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6m3 0V4a2 2 0 012-2h4a2 2 0 012 2v2"/></svg>',
457
+ archive: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="21 8 21 21 3 21 3 8"/><rect x="1" y="3" width="22" height="5"/><line x1="10" y1="12" x2="14" y2="12"/></svg>',
415
458
  };
416
459
 
417
460
  // --- Toast notifications ---
@@ -431,6 +474,7 @@ document.getElementById('btn-fetch').innerHTML = ICONS.download;
431
474
  document.getElementById('btn-find').innerHTML = ICONS.search;
432
475
  document.getElementById('btn-settings').innerHTML = ICONS.settings;
433
476
  document.getElementById('btn-worktree').innerHTML = ICONS.gitBranch + ' <span class="wt-count" style="display:none">0</span>';
477
+ document.getElementById('btn-stash').innerHTML = ICONS.archive + ' <span class="stash-count" style="display:none">0</span>';
434
478
  vscode.postMessage({ command: 'ready' });
435
479
 
436
480
  // --- Message handler ---
@@ -479,9 +523,12 @@ window.addEventListener('message', (event) => {
479
523
  break;
480
524
  case 'loadUncommitted':
481
525
  state.uncommitted = msg.data;
526
+ state.mergeState = msg.data?.mergeState || null;
482
527
  renderCommitList();
528
+ renderMergeBanner();
483
529
  if (state.selectedCommit === 'uncommitted') {
484
- if (!msg.data || (msg.data.staged.length === 0 && msg.data.unstaged.length === 0)) {
530
+ const u = msg.data;
531
+ if (!u || (u.staged.length === 0 && u.unstaged.length === 0 && (!u.conflicted || u.conflicted.length === 0))) {
485
532
  state.selectedCommit = null;
486
533
  state.expandedCommit = null;
487
534
  document.getElementById('detail-panel').classList.add('hidden');
@@ -531,11 +578,19 @@ window.addEventListener('message', (event) => {
531
578
  if (msg.result.ok && (msg.action === 'addWorktree' || msg.action === 'removeWorktree' || msg.action === 'pruneWorktrees')) {
532
579
  vscode.postMessage({ command: 'requestWorktrees' });
533
580
  }
581
+ // Refresh stash list after stash mutations
582
+ if (msg.result.ok && ['stashSave','stashPop','stashDrop','stashApply'].includes(msg.action)) {
583
+ vscode.postMessage({ command: 'requestStashes' });
584
+ }
534
585
  break;
535
586
  case 'loadWorktrees':
536
587
  state.worktrees = msg.data || [];
537
588
  renderWorktreeList();
538
589
  break;
590
+ case 'loadStashes':
591
+ state.stashes = msg.data || [];
592
+ renderStashList();
593
+ break;
539
594
  case 'error':
540
595
  document.getElementById('status-text').textContent = 'Error: ' + msg.message;
541
596
  break;
@@ -552,6 +607,8 @@ document.getElementById('detail-panel').addEventListener('click', (e) => {
552
607
  const file = actionBtn.dataset.file;
553
608
  if (action === 'open') {
554
609
  vscode.postMessage({ command: 'openFile', filePath: file });
610
+ } else if (action === 'open-conflict') {
611
+ vscode.postMessage({ command: 'openConflictFile', filePath: file });
555
612
  } else if (action === 'stage') {
556
613
  vscode.postMessage({ command: 'gitAction', action: 'stage', args: { files: [file] } });
557
614
  } else if (action === 'unstage') {
@@ -829,6 +886,140 @@ function showCreateWorktreeDialog(startPoint) {
829
886
  });
830
887
  }
831
888
 
889
+ // --- Merge/rebase banner ---
890
+ function renderMergeBanner() {
891
+ let banner = document.getElementById('merge-banner');
892
+ if (!state.mergeState) {
893
+ if (banner) banner.remove();
894
+ return;
895
+ }
896
+ const ms = state.mergeState;
897
+ const typeLabel = ms.type === 'cherry-pick' ? 'Cherry-pick' : ms.type.charAt(0).toUpperCase() + ms.type.slice(1);
898
+ const progressText = ms.progress ? ' (' + ms.progress + ')' : '';
899
+ const msgText = ms.message ? ' — ' + escHtml(ms.message) : '';
900
+
901
+ let buttonsHtml = '';
902
+ if (ms.type === 'rebase') {
903
+ buttonsHtml = '<button class="btn-sm btn-continue" data-merge-action="rebaseContinue">Continue</button>'
904
+ + '<button class="btn-sm" data-merge-action="rebaseSkip">Skip</button>'
905
+ + '<button class="btn-sm btn-abort" data-merge-action="rebaseAbort">Abort</button>';
906
+ } else if (ms.type === 'merge') {
907
+ buttonsHtml = '<button class="btn-sm btn-abort" data-merge-action="mergeAbort">Abort</button>';
908
+ } else if (ms.type === 'cherry-pick') {
909
+ buttonsHtml = '<button class="btn-sm btn-continue" data-merge-action="cherryPickContinue">Continue</button>'
910
+ + '<button class="btn-sm btn-abort" data-merge-action="cherryPickAbort">Abort</button>';
911
+ }
912
+
913
+ if (!banner) {
914
+ banner = document.createElement('div');
915
+ banner.id = 'merge-banner';
916
+ banner.className = 'merge-banner';
917
+ const toolbar = document.getElementById('toolbar');
918
+ toolbar.parentNode.insertBefore(banner, toolbar.nextSibling);
919
+ }
920
+ banner.innerHTML = '<span class="banner-icon">⚠</span>'
921
+ + '<span class="banner-text"><strong>' + typeLabel + ' in progress' + progressText + '</strong>' + msgText + '</span>'
922
+ + '<div class="banner-actions">' + buttonsHtml + '</div>';
923
+
924
+ banner.querySelectorAll('[data-merge-action]').forEach(btn => {
925
+ btn.addEventListener('click', () => {
926
+ const action = btn.dataset.mergeAction;
927
+ if (action.includes('Abort')) {
928
+ showDialog({
929
+ title: 'Abort ' + typeLabel,
930
+ message: 'Abort the current ' + ms.type + '? Any resolved conflicts will be lost.',
931
+ destructive: true,
932
+ confirmLabel: 'Abort',
933
+ onConfirm: () => gitAction(action, {}),
934
+ });
935
+ } else {
936
+ gitAction(action, {});
937
+ }
938
+ });
939
+ });
940
+ }
941
+
942
+ // --- Stash popover ---
943
+ const btnStash = document.getElementById('btn-stash');
944
+ const stashPopover = document.getElementById('stash-popover');
945
+
946
+ btnStash.addEventListener('click', (e) => {
947
+ e.stopPropagation();
948
+ const wasHidden = stashPopover.classList.contains('hidden');
949
+ stashPopover.classList.toggle('hidden');
950
+ if (wasHidden) vscode.postMessage({ command: 'requestStashes' });
951
+ });
952
+
953
+ document.addEventListener('click', (e) => {
954
+ if (!e.target.closest('.stash-dropdown')) stashPopover.classList.add('hidden');
955
+ });
956
+
957
+ function renderStashList() {
958
+ const listEl = document.getElementById('stash-list');
959
+ const countEl = btnStash.querySelector('.stash-count');
960
+ const stashes = state.stashes;
961
+ if (countEl) {
962
+ countEl.textContent = stashes.length;
963
+ countEl.style.display = stashes.length > 0 ? '' : 'none';
964
+ }
965
+ if (!stashes.length) {
966
+ listEl.innerHTML = '<div class="stash-empty">No stashes</div>';
967
+ return;
968
+ }
969
+ listEl.innerHTML = stashes.map((s, i) => {
970
+ const ref = 'stash@{' + s.index + '}';
971
+ return '<div class="stash-item">'
972
+ + '<div class="stash-item-info">'
973
+ + '<div class="stash-item-ref">' + escHtml(ref) + '</div>'
974
+ + '<div class="stash-item-msg" title="' + escHtml(s.message) + '">' + escHtml(s.message) + '</div>'
975
+ + '</div>'
976
+ + '<div class="stash-item-actions">'
977
+ + '<button class="stash-apply btn-sm" data-idx="' + i + '" title="Apply (keep stash)">Apply</button>'
978
+ + '<button class="stash-pop btn-sm" data-idx="' + i + '" title="Pop (apply & remove)">Pop</button>'
979
+ + '<button class="stash-drop btn-sm" data-idx="' + i + '" title="Drop (delete)">Drop</button>'
980
+ + '</div>'
981
+ + '</div>';
982
+ }).join('');
983
+
984
+ listEl.querySelectorAll('.stash-apply').forEach(btn => {
985
+ btn.addEventListener('click', (e) => {
986
+ e.stopPropagation();
987
+ const s = state.stashes[parseInt(btn.dataset.idx)];
988
+ if (s) gitAction('stashApply', { stashRef: 'stash@{' + s.index + '}' });
989
+ });
990
+ });
991
+ listEl.querySelectorAll('.stash-pop').forEach(btn => {
992
+ btn.addEventListener('click', (e) => {
993
+ e.stopPropagation();
994
+ const s = state.stashes[parseInt(btn.dataset.idx)];
995
+ if (s) gitAction('stashPop', { stashRef: 'stash@{' + s.index + '}' });
996
+ });
997
+ });
998
+ listEl.querySelectorAll('.stash-drop').forEach(btn => {
999
+ btn.addEventListener('click', (e) => {
1000
+ e.stopPropagation();
1001
+ const s = state.stashes[parseInt(btn.dataset.idx)];
1002
+ if (!s) return;
1003
+ showDialog({
1004
+ title: 'Drop Stash',
1005
+ message: 'Delete stash@{' + s.index + '}? This cannot be undone.',
1006
+ destructive: true,
1007
+ confirmLabel: 'Drop',
1008
+ onConfirm: () => gitAction('stashDrop', { stashRef: 'stash@{' + s.index + '}' }),
1009
+ });
1010
+ });
1011
+ });
1012
+ }
1013
+
1014
+ document.getElementById('stash-save').addEventListener('click', () => {
1015
+ showDialog({
1016
+ title: 'Stash Changes',
1017
+ input: { placeholder: 'Stash message (optional)' },
1018
+ confirmLabel: 'Stash',
1019
+ onConfirm: (msg) => gitAction('stashSave', msg ? { message: msg } : {}),
1020
+ });
1021
+ });
1022
+
832
1023
  // --- Graph column resize ---
833
1024
  {
834
1025
  const resizeHandle = document.getElementById('graph-resize-handle');
@@ -1189,7 +1380,8 @@ function graphVertexOut(e) {
1189
1380
  // --- Commit list ---
1190
1381
  function getDisplayCommits() {
1191
1382
  const u = state.uncommitted;
1192
- if (!u || (u.staged.length === 0 && u.unstaged.length === 0)) return state.commits;
1383
+ const totalFiles = u ? (u.staged.length + u.unstaged.length + (u.conflicted ? u.conflicted.length : 0)) : 0;
1384
+ if (!u || totalFiles === 0) return state.commits;
1193
1385
  const virtualCommit = {
1194
1386
  hash: 'uncommitted',
1195
1387
  parents: state.head ? [state.head] : [],
@@ -1200,7 +1392,7 @@ function getDisplayCommits() {
1200
1392
  committerEmail: '',
1201
1393
  commitDate: Math.floor(Date.now() / 1000),
1202
1394
  refs: [],
1203
- message: 'Uncommitted Changes (' + (u.staged.length + u.unstaged.length) + ' files)',
1395
+ message: 'Uncommitted Changes (' + totalFiles + ' files)',
1204
1396
  };
1205
1397
  return [virtualCommit, ...state.commits];
1206
1398
  }
@@ -1412,9 +1604,25 @@ function renderUncommittedDetail() {
1412
1604
  const u = state.uncommitted;
1413
1605
  if (!u) { panel.classList.add('hidden'); return; }
1414
1606
  let html = '<h3>Uncommitted Changes</h3>';
1415
- if (u.staged.length > 0 || u.unstaged.length > 0) {
1607
+ const hasFiles = u.staged.length > 0 || u.unstaged.length > 0 || (u.conflicted && u.conflicted.length > 0);
1608
+ if (hasFiles) {
1416
1609
  html += fileViewToggleHtml();
1417
1610
  }
1611
+ // Conflict section (above staged/unstaged)
1612
+ if (u.conflicted && u.conflicted.length > 0) {
1613
+ html += '<div class="file-list"><div class="conflict-header">⚠ Conflicts (' + u.conflicted.length + ')</div>';
1614
+ html += u.conflicted.map(f => {
1615
+ const fileName = f.path.split('/').pop() || f.path;
1616
+ return '<div class="file-item">'
1617
+ + '<span class="file-status file-status-U">U</span>'
1618
+ + '<span class="file-clickable" data-file="' + escHtml(f.path) + '" data-hash="uncommitted" data-parent="' + escHtml(state.head) + '">' + escHtml(f.path) + '</span>'
1619
+ + '<div class="file-actions">'
1620
+ + '<button class="file-action-btn" data-action="open-conflict" data-file="' + escHtml(f.path) + '" title="Open conflict file">' + ICONS.fileOpen + '</button>'
1621
+ + '<button class="file-action-btn" data-action="stage" data-file="' + escHtml(f.path) + '" title="Mark resolved (stage)">' + ICONS.plus + '</button>'
1622
+ + '</div></div>';
1623
+ }).join('');
1624
+ html += '</div>';
1625
+ }
1418
1626
  if (u.staged.length > 0) {
1419
1627
  html += '<div class="file-list"><div class="section-actions"><strong>Staged (' + u.staged.length + '):</strong>';
1420
1628
  html += '<button class="btn-sm section-action-btn" data-action="unstage-all">Unstage All</button></div>';
@@ -1427,7 +1635,7 @@ function renderUncommittedDetail() {
1427
1635
  html += renderFileListHtml(u.unstaged, 'uncommitted', state.head, 'unstaged');
1428
1636
  html += '</div>';
1429
1637
  }
1430
- if (u.staged.length === 0 && u.unstaged.length === 0) {
1638
+ if (u.staged.length === 0 && u.unstaged.length === 0 && (!u.conflicted || u.conflicted.length === 0)) {
1431
1639
  html += '<p>No uncommitted changes.</p>';
1432
1640
  }
1433
1641
  html += '<div class="commit-section">';
@@ -1496,6 +1704,16 @@ function showCommitContextMenu(x, y, commit) {
1496
1704
  { label: 'Create Branch Here...', action: () => promptAndAction('Branch name:', (name) => gitAction('createBranch', { name, startPoint: commit.hash })) },
1497
1705
  { label: 'Create Tag Here...', action: () => promptAndAction('Tag name:', (name) => gitAction('createTag', { name, hash: commit.hash })) },
1498
1706
  { label: 'Create Worktree Here...', action: () => showCreateWorktreeDialog(commit.hash) },
1707
+ { separator: true },
1708
+ { label: 'Rebase current branch onto this...', action: () => {
1709
+ showDialog({
1710
+ title: 'Rebase',
1711
+ message: 'Rebase current branch (' + escHtml(state.currentBranch) + ') onto commit ' + commit.hash.substring(0, 7) + '?',
1712
+ rawMessage: true,
1713
+ confirmLabel: 'Rebase',
1714
+ onConfirm: () => gitAction('rebase', { branch: commit.hash }),
1715
+ });
1716
+ }},
1499
1717
  ];
1500
1718
  // Add "Create PR" if PR creation is configured and commit has a branch ref
1501
1719
  if (state.settings.prCreation && state.settings.prCreation.urlTemplate) {
@@ -71,9 +71,14 @@ async function handleMessage(ws: ExtWsSocket, raw: string | Buffer): Promise<voi
71
71
 
72
72
  switch (msg.type) {
73
73
  case "ready": {
74
- // Send current contributions on connect
74
+ // Send current contributions + any activation errors on connect
75
75
  const contributions = contributionRegistry.getAll();
76
- ws.send(JSON.stringify({ type: "contributions:update", contributions } satisfies ExtServerMsg));
76
+ const { extensionService } = await import("../../services/extension.service.ts");
77
+ const activationErrors = Object.fromEntries(extensionService.getActivationErrors());
78
+ const readyMsg: ExtServerMsg = Object.keys(activationErrors).length > 0
79
+ ? { type: "contributions:update", contributions, activationErrors }
80
+ : { type: "contributions:update", contributions };
81
+ ws.send(JSON.stringify(readyMsg));
77
82
  break;
78
83
  }
79
84
 
@@ -81,15 +86,36 @@ async function handleMessage(ws: ExtWsSocket, raw: string | Buffer): Promise<voi
81
86
  try {
82
87
  const { extensionService } = await import("../../services/extension.service.ts");
83
88
  if (extensionService["rpc"]) {
89
+ console.log(`[ExtWS] command:execute "${msg.command}"`);
84
90
  const result = await extensionService["rpc"].sendRequest<{ ok: boolean; error?: string }>(
85
91
  "ext:command:execute", msg.command, ...(msg.args ?? []),
86
92
  );
87
93
  if (!result?.ok) {
88
94
  console.error(`[ExtWS] command:execute failed: ${result?.error ?? "unknown"}`);
95
+ broadcastExtMsg({
96
+ type: "notification",
97
+ id: `cmd-error-${Date.now()}`,
98
+ level: "error",
99
+ message: `Extension command failed: ${result?.error ?? "unknown error"}`,
100
+ });
89
101
  }
102
+ } else {
103
+ console.error(`[ExtWS] command:execute: extension host not ready`);
104
+ broadcastExtMsg({
105
+ type: "notification",
106
+ id: `cmd-error-${Date.now()}`,
107
+ level: "error",
108
+ message: `Extension host not ready. Try reloading the page.`,
109
+ });
90
110
  }
91
111
  } catch (e) {
92
112
  console.error(`[ExtWS] command:execute error:`, e);
113
+ broadcastExtMsg({
114
+ type: "notification",
115
+ id: `cmd-error-${Date.now()}`,
116
+ level: "error",
117
+ message: `Extension command error: ${e instanceof Error ? e.message : String(e)}`,
118
+ });
93
119
  }
94
120
  break;
95
121
  }
@@ -29,6 +29,7 @@ self.addEventListener("message", (event: MessageEvent<RpcMessage>) => {
29
29
 
30
30
  rpc.onRequest("ext:activate", async (params) => {
31
31
  const [extId, entryPath, extensionPath, storedState, baseUrl] = params as [string, string, string, Record<string, Record<string, string | null>>?, string?];
32
+ console.log(`[ExtHost] activating ${extId} from ${entryPath}`);
32
33
  if (activeExtensions.has(extId)) return { ok: true, already: true };
33
34
 
34
35
  // Expose server base URL so extensions can use fetch() with absolute URLs
@@ -68,6 +69,7 @@ rpc.onRequest("ext:activate", async (params) => {
68
69
  window: api.window as WindowService,
69
70
  commands: api.commands as CommandService,
70
71
  });
72
+ console.log(`[ExtHost] activated ${extId} (${activeExtensions.size} total)`);
71
73
  return { ok: true };
72
74
  } catch (e) {
73
75
  const msg = e instanceof Error ? e.message : String(e);
@@ -101,20 +103,23 @@ rpc.onRequest("ext:deactivate", async (params) => {
101
103
 
102
104
  rpc.onRequest("ext:command:execute", async (params) => {
103
105
  const [command, ...args] = params as [string, ...unknown[]];
106
+ console.log(`[ExtHost] command:execute "${command}" (${activeExtensions.size} extensions active)`);
104
107
  for (const [extId, ext] of activeExtensions) {
105
108
  if (ext.commands) {
106
109
  const hasLocal = (ext.commands as any).localHandlers?.has(command);
107
110
  if (!hasLocal) continue;
111
+ console.log(`[ExtHost] routing "${command}" → ${extId}`);
108
112
  try {
109
113
  const result = await (ext.commands as any).executeCommand(command, ...args);
110
114
  return { ok: true, result };
111
115
  } catch (e) {
112
116
  const msg = e instanceof Error ? e.message : String(e);
113
- console.error(`[ExtHost] Command "${command}" in ${extId} threw:`, msg);
117
+ console.error(`[ExtHost] command "${command}" in ${extId} threw:`, msg);
114
118
  return { ok: false, error: msg };
115
119
  }
116
120
  }
117
121
  }
122
+ console.warn(`[ExtHost] command not found: "${command}"`);
118
123
  return { ok: false, error: `Command not found: ${command}` };
119
124
  });
120
125
 
@@ -13,6 +13,7 @@ class ExtensionService {
13
13
  private worker: Worker | null = null;
14
14
  private rpc: RpcChannel | null = null;
15
15
  private activatedIds = new Set<string>();
16
+ private activationErrors = new Map<string, string>();
16
17
  private workerReady = false;
17
18
  private installing = new Set<string>();
18
19
  private extensionPaths = new Map<string, string>();
@@ -56,6 +57,7 @@ class ExtensionService {
56
57
  if (this.worker) { this.worker.terminate(); this.worker = null; }
57
58
  this.workerReady = false;
58
59
  this.activatedIds.clear();
60
+ this.activationErrors.clear();
59
61
  this.extensionPaths.clear();
60
62
  this.bundledIds.clear();
61
63
  contributionRegistry.clear();
@@ -141,15 +143,20 @@ class ExtensionService {
141
143
  const port = cfg.get("port") ?? 8080;
142
144
  const baseUrl = `http://localhost:${port}`;
143
145
 
146
+ console.log(`[ExtService] activating ${id} (entry: ${entryPath})`);
144
147
  const result = await rpc.sendRequest<{ ok: boolean; error?: string }>(
145
148
  "ext:activate", id, entryPath, extDir, storedState, baseUrl,
146
149
  );
147
- if (!result.ok) throw new Error(`Failed to activate ${id}: ${result.error}`);
150
+ if (!result.ok) {
151
+ this.activationErrors.set(id, result.error ?? "Unknown activation error");
152
+ throw new Error(`Failed to activate ${id}: ${result.error}`);
153
+ }
148
154
 
155
+ this.activationErrors.delete(id);
149
156
  this.activatedIds.add(id);
150
157
  if (manifest.contributes) contributionRegistry.register(id, manifest.contributes);
151
158
  this.broadcastContributions();
152
- console.log(`[ExtService] Activated ${id}`);
159
+ console.log(`[ExtService] activated ${id} successfully`);
153
160
  }
154
161
 
155
162
  async deactivate(id: string): Promise<void> {
@@ -236,7 +243,9 @@ class ExtensionService {
236
243
  }
237
244
  for (const row of getExtensions()) {
238
245
  if (row.enabled !== 1) continue;
246
+ console.log(`[ExtService] startup: activating ${row.id}...`);
239
247
  try { await this.activate(row.id); } catch (e) {
248
+ this.activationErrors.set(row.id, e instanceof Error ? e.message : String(e));
240
249
  console.error(`[ExtService] Failed to activate ${row.id} on startup:`, e);
241
250
  }
242
251
  }
@@ -252,12 +261,17 @@ class ExtensionService {
252
261
  isActivated(id: string): boolean { return this.activatedIds.has(id); }
253
262
  isBundled(id: string): boolean { return this.bundledIds.has(id); }
254
263
  getExtensionsDir(): string { return resolve(getPpmDir(), "extensions"); }
264
+ getActivationErrors(): Map<string, string> { return new Map(this.activationErrors); }
255
265
 
256
266
  /** Push current contributions to all connected browser clients */
257
267
  private broadcastContributions(): void {
258
268
  try {
259
269
  const { broadcastExtMsg } = require("../server/ws/extensions.ts");
260
- broadcastExtMsg({ type: "contributions:update", contributions: contributionRegistry.getAll() });
270
+ const contributions = contributionRegistry.getAll();
271
+ broadcastExtMsg(this.activationErrors.size > 0
272
+ ? { type: "contributions:update", contributions, activationErrors: Object.fromEntries(this.activationErrors) }
273
+ : { type: "contributions:update", contributions },
274
+ );
261
275
  } catch {}
262
276
  }
263
277
  }
@@ -51,7 +51,7 @@ export type ExtServerMsg =
51
51
  | { type: "webview:postMessage"; panelId: string; message: unknown }
52
52
  | { type: "tab:open"; tabType: string; title: string; projectId: string | null; closable?: boolean; metadata?: Record<string, unknown> }
53
53
  | { type: "project:switch"; projectName: string }
54
- | { type: "contributions:update"; contributions: ExtensionContributes };
54
+ | { type: "contributions:update"; contributions: ExtensionContributes; activationErrors?: Record<string, string> };
55
55
 
56
56
  // --- Client → Server messages ---
57
57