dsh-ptc-plus 0.2.4 → 0.3.1

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 (37) hide show
  1. package/README.en.md +171 -0
  2. package/README.md +79 -69
  3. package/assets/ptc-plus-repair-en.png +0 -0
  4. package/assets/ptc-plus-settings-en.png +0 -0
  5. package/assets/ptc-plus-settings-zh.png +0 -0
  6. package/client.js +1134 -144
  7. package/docs/adr/0019-plugin-settings-and-kill-switch.md +19 -4
  8. package/docs/adr/0020-optional-cordis-tools-in-ptc-mode.md +2 -2
  9. package/docs/adr/0022-stage-npm-releases-with-oidc.md +2 -2
  10. package/docs/architecture.md +10 -9
  11. package/docs/client-ui.md +29 -11
  12. package/docs/installation.md +11 -3
  13. package/docs/runtime-reference.md +17 -11
  14. package/docs/value-wire.md +1 -1
  15. package/index.js +30 -1
  16. package/internal/cell-analysis.js +20 -3
  17. package/internal/cell-rewriter.js +36 -2
  18. package/internal/config-spec.js +182 -117
  19. package/internal/direct-surface-owner.js +56 -5
  20. package/internal/edit-transport-owner.js +10 -1
  21. package/internal/failure-reporting.js +15 -0
  22. package/internal/rejected-cell-editor.js +21 -12
  23. package/internal/repl-memory-projection.js +277 -0
  24. package/internal/run-code-description.js +34 -0
  25. package/internal/runtime-bridge-owner.js +63 -15
  26. package/internal/runtime-config.js +2 -0
  27. package/internal/session-cell-executor.js +14 -4
  28. package/internal/session-events.js +7 -0
  29. package/internal/session-journal.js +3 -2
  30. package/internal/session-log-view.js +2 -1
  31. package/internal/session-runtime.js +19 -2
  32. package/internal/session-state.js +83 -4
  33. package/package.json +4 -3
  34. package/src/client-activity.js +472 -0
  35. package/src/client.js +456 -20
  36. package/README.zh.md +0 -161
  37. /package/assets/{ptc-plus-repair.png → ptc-plus-repair-zh.png} +0 -0
package/src/client.js CHANGED
@@ -1,4 +1,9 @@
1
- import { CONFIG_FIELDS, SETTINGS_NAMESPACE } from '../internal/config-spec.js'
1
+ import { CONFIG_FIELDS, CONFIG_GROUPS, SETTINGS_NAMESPACE } from '../internal/config-spec.js'
2
+ import { derivePtcToolView } from './client-activity.js'
3
+ import {
4
+ normalizeReplMemorySnapshot,
5
+ unavailableReplMemorySnapshot,
6
+ } from '../internal/repl-memory-projection.js'
2
7
 
3
8
  const CLIENT_STYLE_ID = 'ptc-plus-client-style'
4
9
  const CLIENT_CSS = `
@@ -7,10 +12,24 @@ const CLIENT_CSS = `
7
12
  .ptcPlusHeader:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(38,49,72,.06))}.ptcPlusHeader:focus-visible,.ptcPlusButton:focus-visible,.ptcPlusInput:focus-visible{outline:2px solid var(--dsw-alias-interactive-primary,#4d6bfe);outline-offset:-2px}
8
13
  .ptcPlusHeadText{display:flex;flex:1;min-width:0;flex-direction:column;align-items:flex-start;gap:1px}.ptcPlusName{font-size:14px;font-weight:600;line-height:20px}.ptcPlusDescription{color:var(--dsw-alias-label-tertiary,#74777d);font-size:12px;line-height:18px;overflow-wrap:anywhere}.ptcPlusStatus{display:inline-flex;align-items:center;flex:none;padding:2px 8px;border-radius:999px;font-size:11px;font-weight:500;line-height:16px}.ptcPlusStatus[data-enabled=true]{color:var(--dsw-alias-state-success-primary,#16794f);background:var(--dsw-alias-state-success-tertiary,#e7f7ef)}.ptcPlusStatus[data-enabled=false]{color:var(--dsw-alias-label-tertiary,#74777d);background:var(--dsw-alias-interactive-bg-hover,rgba(38,49,72,.06))}
9
14
  .ptcPlusChevron{display:flex;color:var(--dsw-alias-label-tertiary,#74777d);transition:transform .18s ease}.ptcPlusChevron[data-open=true]{transform:rotate(180deg)}.ptcPlusBody{display:grid;grid-template-rows:0fr;transition:grid-template-rows .2s ease}.ptcPlusBody[data-open=true]{grid-template-rows:1fr}.ptcPlusBodyInner{min-height:0;overflow:hidden}.ptcPlusFields{margin:0 16px;padding:8px 0 12px;border-top:1px solid var(--dsw-alias-border-l2,rgba(0,0,0,.1))}
10
- .ptcPlusRow{display:flex;align-items:center;gap:12px;min-height:48px;border-top:1px solid var(--dsw-alias-border-l2,rgba(0,0,0,.1))}.ptcPlusRow:first-child{border-top:0}.ptcPlusMain{flex:1;min-width:0}.ptcPlusLabel{font-size:14px;font-weight:500;line-height:20px}.ptcPlusDetail,.ptcPlusMessage{color:var(--dsw-alias-label-tertiary,#74777d);font-size:12px;line-height:18px;overflow-wrap:anywhere}.ptcPlusInput{box-sizing:border-box;min-width:72px;width:140px;padding:5px 8px;border:1px solid var(--dsw-alias-border-l2,rgba(0,0,0,.1));border-radius:6px;background:var(--dsw-alias-bg-layer-1,#fff);color:inherit;font:12px/18px ui-monospace,SFMono-Regular,Consolas,monospace}.ptcPlusCheck{width:18px;height:18px;accent-color:var(--dsw-alias-interactive-primary,#4d6bfe)}
11
- .ptcPlusFooter{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-top:8px}.ptcPlusButton{min-height:32px;padding:0 12px;border:1px solid var(--dsw-alias-border-l2,rgba(0,0,0,.1));border-radius:6px;background:transparent;color:inherit;cursor:pointer;font:500 13px/20px inherit;transition:background-color .16s ease,border-color .16s ease}.ptcPlusButton:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover,rgba(38,49,72,.06))}.ptcPlusButton:disabled,.ptcPlusInput:disabled,.ptcPlusCheck:disabled{cursor:not-allowed;opacity:.55}.ptcPlusActive{display:inline-flex;align-items:center;gap:5px;color:var(--dsw-alias-state-success-primary,#16794f);font-size:12px;line-height:18px;white-space:nowrap}
12
- @media(max-width:560px){.ptcPlusHeader{padding:12px}.ptcPlusFields{margin:0 12px}.ptcPlusRow{align-items:flex-start;flex-direction:column;gap:6px;padding:10px 0}.ptcPlusInput{width:100%}.ptcPlusFooter{align-items:stretch;flex-direction:column}.ptcPlusButton{width:100%}}
13
- @media(prefers-reduced-motion:reduce){.ptcPlusHeader,.ptcPlusChevron,.ptcPlusBody,.ptcPlusButton{transition:none}}
15
+ .ptcPlusGroup+.ptcPlusGroup{margin-top:14px}.ptcPlusGroupTitle{margin:0;padding:8px 0 5px;color:var(--dsw-alias-label-secondary,#52565d);font-size:11px;font-weight:600;letter-spacing:0;line-height:16px}.ptcPlusRow{display:flex;align-items:center;gap:12px;min-height:48px;border-top:1px solid var(--dsw-alias-border-l2,rgba(0,0,0,.1))}.ptcPlusRow:first-child{border-top:0}.ptcPlusMain{flex:1;min-width:0}.ptcPlusLabel{font-size:14px;font-weight:500;line-height:20px}.ptcPlusDetail,.ptcPlusMessage{color:var(--dsw-alias-label-tertiary,#74777d);font-size:12px;line-height:18px;overflow-wrap:anywhere}.ptcPlusInput{box-sizing:border-box;min-width:72px;width:140px;padding:5px 8px;border:1px solid var(--dsw-alias-border-l2,rgba(0,0,0,.1));border-radius:6px;background:var(--dsw-alias-bg-layer-1,#fff);color:inherit;font:12px/18px ui-monospace,SFMono-Regular,Consolas,monospace}.ptcPlusCheck{width:18px;height:18px;accent-color:var(--dsw-alias-interactive-primary,#4d6bfe)}
16
+ .ptcPlusFooter{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-top:8px}.ptcPlusButton{min-height:32px;padding:0 12px;border:1px solid var(--dsw-alias-border-l2,rgba(0,0,0,.1));border-radius:6px;background:transparent;color:inherit;cursor:pointer;font:500 13px/20px inherit;transition:background-color .16s ease,border-color .16s ease}.ptcPlusButton:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover,rgba(38,49,72,.06))}.ptcPlusButton:disabled,.ptcPlusInput:disabled,.ptcPlusCheck:disabled{cursor:not-allowed;opacity:.55}
17
+ .ptcPlusActiveShell{display:inline-flex;align-items:center}.ptcPlusActive{appearance:none;display:inline-flex;height:24px;align-items:center;gap:5px;padding:0 8px;border:1px solid color-mix(in srgb,var(--dsw-alias-state-success-primary,#16794f) 32%,transparent);border-radius:6px;background:var(--dsw-alias-state-success-tertiary,#e7f7ef);color:var(--dsw-alias-state-success-primary,#16794f);cursor:help;font:600 12px/18px inherit;white-space:nowrap;transition:background-color .14s ease,border-color .14s ease}.ptcPlusActive:hover,.ptcPlusActive[aria-expanded=true]{border-color:color-mix(in srgb,var(--dsw-alias-state-success-primary,#16794f) 48%,transparent);background:color-mix(in srgb,var(--dsw-alias-state-success-primary,#16794f) 16%,var(--dsw-alias-bg-layer-3,#fff))}.ptcPlusActive:focus-visible{outline:2px solid var(--dsw-alias-state-success-primary,#16794f);outline-offset:2px}.ptcPlusReplPopover{position:fixed;z-index:2147483000;inset:auto;display:none;box-sizing:border-box;margin:0;padding:0;border:0;overflow:visible;background:transparent;color:var(--dsw-alias-label-primary,#18191c)}.ptcPlusReplPopover:popover-open,.ptcPlusReplPopover[data-open=true]{display:block}.ptcPlusReplPopover::backdrop{background:transparent}.ptcPlusReplCard{display:flex;max-height:inherit;overflow:hidden;flex-direction:column;border:1px solid color-mix(in srgb,var(--dsw-alias-state-success-primary,#16794f) 22%,var(--dsw-alias-border-l2,rgba(0,0,0,.1)));border-top:3px solid var(--dsw-alias-state-success-primary,#16794f);border-radius:8px;background:var(--dsw-alias-bg-layer-3,#fff);box-shadow:0 14px 36px rgba(16,24,40,.2),0 3px 10px rgba(16,24,40,.1);color:var(--dsw-alias-label-primary,#18191c);white-space:normal}.ptcPlusReplHead{display:grid;grid-template-columns:auto minmax(0,1fr);align-items:center;column-gap:8px;padding:11px 13px 10px;border-bottom:1px solid var(--dsw-alias-border-l2,rgba(0,0,0,.1));background:color-mix(in srgb,var(--dsw-alias-state-success-primary,#16794f) 7%,var(--dsw-alias-bg-layer-3,#fff))}.ptcPlusReplStatusDot{grid-row:1/3;width:8px;height:8px;border-radius:50%;background:var(--dsw-alias-state-success-primary,#16794f);box-shadow:0 0 0 3px color-mix(in srgb,var(--dsw-alias-state-success-primary,#16794f) 14%,transparent)}.ptcPlusReplTitle,.ptcPlusReplSummary{display:block;min-width:0}.ptcPlusReplTitle{font-size:13px;font-weight:600;line-height:19px}.ptcPlusReplSummary{color:var(--dsw-alias-label-tertiary,#74777d);font-size:11px;line-height:16px}.ptcPlusReplList{min-height:0;margin:0;padding:5px 0;overflow:auto;overscroll-behavior:contain;list-style:none;scrollbar-gutter:stable}.ptcPlusReplBinding{display:grid;grid-template-columns:minmax(0,1fr) auto;align-items:center;gap:4px 10px;padding:7px 12px}.ptcPlusReplBinding:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(38,49,72,.06))}.ptcPlusReplIdentity{display:flex;min-width:0;align-items:center;gap:7px}.ptcPlusReplName{min-width:0;overflow:hidden;font:12px/18px ui-monospace,SFMono-Regular,Consolas,monospace;text-overflow:ellipsis;white-space:nowrap}.ptcPlusReplKind{flex:none;padding:1px 6px;border:1px solid color-mix(in srgb,currentColor 22%,transparent);border-radius:999px;background:color-mix(in srgb,currentColor 10%,transparent);font-size:10px;font-weight:600;line-height:15px}.ptcPlusReplKind[data-kind=variable]{color:var(--dsw-alias-interactive-primary,#315fbd)}.ptcPlusReplKind[data-kind=function]{color:#7651b5}.ptcPlusReplKind[data-kind=class]{color:var(--dsw-alias-state-warning-primary,#946200)}.ptcPlusReplKind[data-kind=import]{color:#14766f}.ptcPlusReplPreview{grid-column:1;min-width:0;overflow:hidden;color:var(--dsw-alias-label-tertiary,#74777d);font:11px/16px ui-monospace,SFMono-Regular,Consolas,monospace;text-overflow:ellipsis;white-space:nowrap}.ptcPlusReplInspect{grid-column:2;grid-row:1/3;display:inline-flex;align-items:center;gap:4px;padding:3px 5px;border:0;border-radius:4px;background:transparent;color:var(--dsw-alias-label-secondary,#52565d);cursor:pointer;font:500 11px/17px inherit;white-space:nowrap}.ptcPlusReplInspect:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(38,49,72,.06));color:var(--dsw-alias-interactive-primary,#4d6bfe)}.ptcPlusReplInspect:focus-visible{outline:2px solid var(--dsw-alias-interactive-primary,#4d6bfe);outline-offset:1px}.ptcPlusReplDefinition{grid-column:1/-1;min-width:0;margin-top:4px;padding:8px;border-left:2px solid var(--dsw-alias-interactive-primary,#4d6bfe);background:var(--dsw-alias-bg-layer-2,rgba(38,49,72,.03))}.ptcPlusReplLocation{display:block;margin-bottom:5px;color:var(--dsw-alias-label-tertiary,#74777d);font-size:10px;line-height:15px}.ptcPlusReplCode{max-height:180px;margin:0;overflow:auto;color:inherit;font:11px/16px ui-monospace,SFMono-Regular,Consolas,monospace;white-space:pre-wrap;overflow-wrap:anywhere}.ptcPlusReplEmpty,.ptcPlusReplMore{display:block;color:var(--dsw-alias-label-tertiary,#74777d)}.ptcPlusReplEmpty{padding:18px 13px;font-size:12px;line-height:18px}.ptcPlusReplMore{padding:8px 13px;border-top:1px solid var(--dsw-alias-border-l2,rgba(0,0,0,.1));background:var(--dsw-alias-bg-layer-2,rgba(38,49,72,.03));font-size:11px;line-height:17px}
18
+ .ptcPlusReplList{max-height:min(52vh,480px)}.ptcPlusReplBinding{grid-template-columns:minmax(0,1fr) 24px;gap:3px 8px;min-height:36px;padding:5px 12px;content-visibility:auto;contain-intrinsic-size:36px;cursor:pointer;transition:background-color .16s ease}.ptcPlusReplBinding:focus-visible{outline:2px solid var(--dsw-alias-interactive-primary,#4d6bfe);outline-offset:-2px}.ptcPlusReplBinding[data-expanded=true]{background:color-mix(in srgb,var(--dsw-alias-interactive-primary,#4d6bfe) 5%,transparent)}.ptcPlusReplName{grid-column:1}.ptcPlusReplName[data-kind=variable]{color:var(--dsw-alias-interactive-primary,#315fbd)}.ptcPlusReplName[data-kind=function]{color:#7651b5}.ptcPlusReplName[data-kind=class]{color:var(--dsw-alias-state-warning-primary,#946200)}.ptcPlusReplName[data-kind=import]{color:#14766f}.ptcPlusReplPreview{grid-column:1}.ptcPlusReplChevron{grid-column:2;grid-row:1/3;display:flex;align-items:center;justify-content:center;color:var(--dsw-alias-label-tertiary,#74777d);transition:transform .2s ease}.ptcPlusReplChevron[data-open=true]{transform:rotate(180deg)}.ptcPlusReplDefinitionWrap{grid-column:1/-1;display:grid;grid-template-rows:0fr;min-width:0;transition:grid-template-rows .24s cubic-bezier(.2,.7,.2,1)}.ptcPlusReplDefinitionWrap[data-open=true]{grid-template-rows:1fr}.ptcPlusReplDefinitionInner{min-height:0;overflow:hidden}
19
+ .ptcPlusTool{display:flex;min-width:0;flex-direction:column}.ptcPlusToolPreview{display:flex;min-width:0;flex:1 1 auto;flex-direction:row;align-items:center;overflow:hidden;margin-left:7px}.ptcPlusToolPreview .ptcPlusFeatures{flex:0 1 auto;flex-wrap:nowrap;overflow:hidden;margin:0 0 0 7px}.ptcPlusToolSummaryLine{box-sizing:border-box;display:flex;min-width:0;min-height:20px;flex:1 1 auto;align-items:center;gap:7px;padding:0;color:inherit;line-height:20px}.ptcPlusToolSummary{box-sizing:border-box;display:flex;min-width:0;min-height:32px;align-items:center;gap:7px;padding:0;color:inherit;line-height:20px}.ptcPlusToolSummary[data-expandable=true]{cursor:pointer}.ptcPlusToolSummary[data-expandable=true]:hover .ptcPlusToolTitle{color:var(--dsw-alias-interactive-primary,#4d6bfe)}.ptcPlusToolSummary:focus-visible{outline:2px solid var(--dsw-alias-interactive-primary,#4d6bfe);outline-offset:2px}.ptcPlusToolLeading{display:flex;width:16px;height:20px;flex:none;align-items:center;justify-content:center;color:var(--dsw-alias-label-tertiary,#74777d)}.ptcPlusToolChevron{transition:transform .16s ease}.ptcPlusToolChevron[data-open=true]{transform:rotate(180deg)}.ptcPlusToolTitle{display:flex;height:20px;flex:none;align-items:center;font-size:13px;font-weight:500;line-height:20px}.ptcPlusToolState{display:flex;height:20px;flex:none;align-items:center;color:var(--dsw-alias-label-tertiary,#74777d);font-size:11px;line-height:20px}.ptcPlusToolSummaryLine[data-state=running] .ptcPlusToolState,.ptcPlusToolSummary[data-state=running] .ptcPlusToolState{color:var(--dsw-alias-interactive-primary,#4d6bfe)}.ptcPlusToolSummaryLine[data-state=error] .ptcPlusToolState,.ptcPlusToolSummary[data-state=error] .ptcPlusToolState{color:var(--dsw-alias-state-danger-primary,#c43d3d)}.ptcPlusToolSummaryLine[data-state=stopped] .ptcPlusToolState,.ptcPlusToolSummary[data-state=stopped] .ptcPlusToolState{color:var(--dsw-alias-state-warning-primary,#a15c00)}.ptcPlusToolSep{width:3px;height:3px;flex:none;border-radius:50%;background:var(--dsw-alias-label-tertiary,#74777d)}.ptcPlusToolDescription{display:flex;min-width:0;min-height:20px;flex:1 1 auto;align-items:center;overflow:hidden;color:var(--dsw-alias-label-secondary,#52565d);font-size:13px;line-height:20px;text-overflow:ellipsis;white-space:nowrap}.ptcPlusToolPreview .ptcPlusFeature{flex:none;max-width:180px;white-space:nowrap}.ptcPlusToolPreview .ptcPlusFeatureDetail{max-width:120px}.ptcPlusToolSummaryLine[data-state=error] .ptcPlusToolDescription,.ptcPlusToolSummary[data-state=error] .ptcPlusToolDescription{color:var(--dsw-alias-state-danger-primary,#c43d3d)}.ptcPlusToolSummaryLine[data-state=stopped] .ptcPlusToolDescription,.ptcPlusToolSummary[data-state=stopped] .ptcPlusToolDescription{color:var(--dsw-alias-state-warning-primary,#a15c00)}
20
+ .ptcPlusFeatures{display:flex;min-width:0;flex-wrap:wrap;gap:3px 14px;margin:0 0 5px 23px}.ptcPlusFeature{display:inline-flex;min-width:0;align-items:center;gap:5px;color:var(--dsw-alias-label-secondary,#52565d);font-size:11px;line-height:17px}.ptcPlusFeature::before{width:4px;height:4px;flex:none;border-radius:50%;background:var(--dsw-alias-interactive-primary,#4d6bfe);content:''}.ptcPlusFeatureName{font-weight:500}.ptcPlusFeatureDetail{min-width:0;overflow:hidden;color:var(--dsw-alias-label-tertiary,#74777d);font-family:ui-monospace,SFMono-Regular,Consolas,monospace;text-overflow:ellipsis;white-space:nowrap}
21
+ .ptcPlusToolBody{margin:4px 0 8px 23px;border-left:2px solid var(--dsw-alias-border-l2,rgba(0,0,0,.1));background:var(--dsw-alias-bg-layer-2,rgba(38,49,72,.03))}.ptcPlusToolSection{display:flex;min-width:0;flex-direction:column;gap:4px;padding:9px 11px}.ptcPlusToolSection+.ptcPlusToolSection{border-top:1px solid var(--dsw-alias-border-l2,rgba(0,0,0,.1))}.ptcPlusToolSectionLabel{color:var(--dsw-alias-label-tertiary,#74777d);font-size:10px;font-weight:600;line-height:16px;text-transform:uppercase}.ptcPlusToolCode{max-height:320px;margin:0;overflow:auto;color:inherit;font:12px/18px ui-monospace,SFMono-Regular,Consolas,monospace;white-space:pre-wrap;overflow-wrap:anywhere}.ptcPlusIoCard{display:flex;flex-direction:column;border:1px solid var(--dsw-alias-border-l1,rgba(0,0,0,.1));border-radius:12px;background:var(--dsw-alias-markdown-code-block,rgba(38,49,72,.06));overflow:hidden}.ptcPlusIoText{max-height:320px;margin:0;padding:12px 16px;overflow:auto;color:var(--dsw-alias-label-secondary,#52565d);font:12px/18px ui-monospace,SFMono-Regular,Consolas,monospace;white-space:pre-wrap;overflow-wrap:anywhere}.ptcPlusIoText[data-error]{color:var(--dsw-alias-state-error-primary,#c43d3d)}.ptcPlusInspect{display:inline-flex;align-self:flex-start;align-items:center;gap:4px;margin:4px 0 2px 4px;padding:2px 8px;border:1px solid var(--dsw-alias-border-l2,rgba(0,0,0,.1));border-radius:999px;background:var(--dsw-alias-bg-base,#fff);color:var(--dsw-alias-label-secondary,#52565d);cursor:pointer;opacity:0;font-size:11px;line-height:16px;transition:opacity .1s ease;display:inline-flex}.ptcPlusTool:hover .ptcPlusInspect,.ptcPlusInspect:focus-visible{opacity:1}.ptcPlusInspect:hover{background:var(--dsw-alias-interactive-bg-hover-solid,rgba(38,49,72,.06));color:var(--dsw-alias-label-primary,#18191c)}
22
+ @media(max-width:560px){.ptcPlusHeader{padding:12px}.ptcPlusFields{margin:0 12px}.ptcPlusRow{align-items:flex-start;flex-direction:column;gap:6px;padding:10px 0}.ptcPlusInput{width:100%}.ptcPlusFooter{align-items:stretch;flex-direction:column}.ptcPlusButton{width:100%}.ptcPlusFeatures,.ptcPlusToolBody{margin-left:0}.ptcPlusToolSummary .ptcPlusToolDescription{white-space:normal;overflow-wrap:anywhere}}
23
+ @media(prefers-reduced-motion:reduce){.ptcPlusHeader,.ptcPlusChevron,.ptcPlusBody,.ptcPlusButton,.ptcPlusActive,.ptcPlusToolChevron,.ptcPlusReplChevron,.ptcPlusReplDefinitionWrap,.ptcPlusInspect{transition:none}}
24
+ /* The summary button owns disclosure; definition content is a separate grid item. */
25
+ .ptcPlusReplBinding{padding:0;cursor:default}.ptcPlusReplBindingTrigger{appearance:none;display:grid;width:100%;grid-column:1/-1;grid-template-columns:minmax(0,1fr) 24px;gap:3px 8px;min-height:36px;padding:5px 12px;border:0;background:transparent;color:inherit;text-align:left;cursor:pointer;font:inherit;transition:background-color .16s ease}.ptcPlusReplBindingTrigger:hover,.ptcPlusReplBindingTrigger[aria-expanded=true]{background:color-mix(in srgb,var(--dsw-alias-interactive-primary,#4d6bfe) 5%,transparent)}.ptcPlusReplBindingTrigger:focus-visible{outline:2px solid var(--dsw-alias-interactive-primary,#4d6bfe);outline-offset:-2px}
26
+ /* High-contrast TypeScript-like token colors adapt to the active text theme. */
27
+ .ptcPlusReplCard .ptcPlusReplName[data-kind=variable]{color:color-mix(in srgb,#005cc5 78%,var(--dsw-alias-label-primary,#18191c))}
28
+ .ptcPlusReplCard .ptcPlusReplName[data-kind=function]{color:color-mix(in srgb,#795e26 78%,var(--dsw-alias-label-primary,#18191c))}
29
+ .ptcPlusReplCard .ptcPlusReplName[data-kind=class]{color:color-mix(in srgb,#267f99 78%,var(--dsw-alias-label-primary,#18191c))}
30
+ .ptcPlusReplCard .ptcPlusReplName[data-kind=import]{color:color-mix(in srgb,#af00db 78%,var(--dsw-alias-label-primary,#18191c))}
31
+ /* Keep the session-header action on the same compact 32px rhythm as DSH chrome. */
32
+ .ptcPlusActiveShell{display:inline-flex;height:28px;align-items:center;justify-content:center;line-height:0;vertical-align:middle}.ptcPlusActive{box-sizing:border-box;height:28px;justify-content:center;gap:6px;padding:0 6px;border:0;background:transparent;font-family:inherit;font-size:13px;font-weight:500;line-height:18px}.ptcPlusActive::before{width:6px;height:6px;flex:none;border-radius:50%;background:currentColor;box-shadow:0 0 0 2px color-mix(in srgb,currentColor 18%,transparent);content:''}.ptcPlusActive:hover,.ptcPlusActive[aria-expanded=true]{background:var(--dsw-alias-interactive-bg-hover,rgba(38,49,72,.06))}.ptcPlusActiveLabel{display:inline-flex;height:18px;align-items:center;line-height:18px}
14
33
  `
15
34
 
16
35
  /** Locale namespace owning every settings-card string (field copy plus chrome). */
@@ -31,7 +50,33 @@ const CHROME_COPY = Object.freeze({
31
50
  'status.applied': '设置已立即生效',
32
51
  'status.conflict': '设置未生效,请检查设置冲突',
33
52
  'status.failed': '设置失败:{error}',
34
- 'indicator.title': 'PTC Plus 已启用',
53
+ 'indicator.title': 'PTC Plus 已启用;查看当前可复用的 REPL 绑定',
54
+ 'memory.title': 'REPL 可复用绑定',
55
+ 'memory.count': '{count} 个可复用绑定',
56
+ 'memory.empty': '当前没有可复用绑定',
57
+ 'memory.unavailable': '当前绑定状态尚不可确认',
58
+ 'memory.more': '另有 {count} 个绑定未显示',
59
+ 'memory.kind.variable': '变量',
60
+ 'memory.kind.function': '函数',
61
+ 'memory.kind.class': '类',
62
+ 'memory.kind.import': '导入',
63
+ 'memory.location': '第 {line} 行,第 {column} 列',
64
+ 'tool.code': '执行',
65
+ 'tool.codeEdit': '修正执行',
66
+ 'tool.running': '正在运行',
67
+ 'tool.completed': '执行完成',
68
+ 'tool.failed': '执行失败',
69
+ 'tool.stopped': '执行已中断',
70
+ 'tool.source': '源码',
71
+ 'tool.result': '结果',
72
+ 'tool.copy': '复制代码',
73
+ 'tool.copied': '已复制',
74
+ 'tool.inspect': '检查调用',
75
+ 'feature.safeEdit': '安全编辑执行',
76
+ 'feature.codeRun': '隔离执行 code.run',
77
+ 'feature.stateSaved': '保存 REPL 状态',
78
+ 'feature.stateRestored': '恢复 REPL 状态',
79
+ 'feature.stateDeleted': '删除 REPL 状态',
35
80
  }),
36
81
  en: Object.freeze({
37
82
  'card.description': 'The session-bound TypeScript REPL for PTC mode.',
@@ -46,7 +91,33 @@ const CHROME_COPY = Object.freeze({
46
91
  'status.applied': 'Setting applied immediately.',
47
92
  'status.conflict': 'The setting did not take effect; check for conflicting settings.',
48
93
  'status.failed': 'Could not save: {error}',
49
- 'indicator.title': 'PTC Plus is active',
94
+ 'indicator.title': 'PTC Plus is active; view reusable REPL bindings',
95
+ 'memory.title': 'Reusable REPL bindings',
96
+ 'memory.count': '{count} reusable bindings',
97
+ 'memory.empty': 'No reusable bindings',
98
+ 'memory.unavailable': 'Current binding state cannot be confirmed',
99
+ 'memory.more': '{count} more bindings not shown',
100
+ 'memory.kind.variable': 'Variable',
101
+ 'memory.kind.function': 'Function',
102
+ 'memory.kind.class': 'Class',
103
+ 'memory.kind.import': 'Import',
104
+ 'memory.location': 'Line {line}, column {column}',
105
+ 'tool.code': 'Code',
106
+ 'tool.codeEdit': 'Code edit',
107
+ 'tool.running': 'Running',
108
+ 'tool.completed': 'Completed',
109
+ 'tool.failed': 'Failed',
110
+ 'tool.stopped': 'Stopped',
111
+ 'tool.source': 'Source',
112
+ 'tool.result': 'Result',
113
+ 'tool.inspect': 'Inspect call',
114
+ 'feature.safeEdit': 'Safe edit execution',
115
+ 'feature.codeRun': 'Isolated code.run execution',
116
+ 'feature.stateSaved': 'Saved REPL state',
117
+ 'feature.stateRestored': 'Restored REPL state',
118
+ 'feature.stateDeleted': 'Deleted REPL state',
119
+ 'tool.copy': 'Copy code',
120
+ 'tool.copied': 'Copied',
50
121
  }),
51
122
  })
52
123
 
@@ -62,6 +133,9 @@ function fieldCopy(field, locale) {
62
133
  const SETTINGS_COPY = Object.freeze(Object.fromEntries(
63
134
  ['zh', 'en'].map(locale => [locale, Object.freeze({
64
135
  ...CHROME_COPY[locale],
136
+ ...Object.assign({}, ...CONFIG_GROUPS.map(group => ({
137
+ [`group.${group.key}`]: locale === 'en' ? group.labelEn : group.label,
138
+ }))),
65
139
  ...Object.assign({}, ...CONFIG_FIELDS.map(field => fieldCopy(field, locale))),
66
140
  })]),
67
141
  ))
@@ -77,8 +151,11 @@ window.__ModuleLoader__.load({
77
151
  factory: (require) => {
78
152
  const React = require('react')
79
153
  const {
154
+ CodeBlock,
155
+ DisclosureRow,
80
156
  IconCheckOutline14,
81
157
  IconChevronDownOutline14,
158
+ IconInspectOutline12,
82
159
  } = require('@deepseek-ai/dsh-client-ui-primitives')
83
160
  const module = { exports: {} }
84
161
  const h = React.createElement
@@ -163,6 +240,24 @@ window.__ModuleLoader__.load({
163
240
  const fieldDisabled = field => unavailable
164
241
  || pending.has(field.key)
165
242
  || (field.key !== 'enabled' && !enabled)
243
+ const settingGroups = CONFIG_GROUPS.map(group => h('section', {
244
+ key: group.key,
245
+ className: 'ptcPlusGroup',
246
+ 'aria-labelledby': `ptc-plus-settings-group-${group.key}`,
247
+ },
248
+ h('h3', {
249
+ id: `ptc-plus-settings-group-${group.key}`,
250
+ className: 'ptcPlusGroupTitle',
251
+ }, t(`group.${group.key}`)),
252
+ ...group.fields.map(key => {
253
+ const field = CONFIG_FIELDS.find(candidate => candidate.key === key)
254
+ if (field === undefined) return null
255
+ return h('div', { key: field.key, className: 'ptcPlusRow' },
256
+ h('div', { className: 'ptcPlusMain' },
257
+ h('div', { className: 'ptcPlusLabel' }, t(`${field.key}.label`)),
258
+ field.description === '' ? null : h('div', { className: 'ptcPlusDetail' }, t(`${field.key}.description`))),
259
+ fieldInput(field, value[field.key], fieldDisabled(field), persist, t(`${field.key}.label`)))
260
+ })))
166
261
  return h('li', { className: 'ptcPlusCard' },
167
262
  h('button', {
168
263
  type: 'button', className: 'ptcPlusHeader', 'aria-expanded': open,
@@ -181,11 +276,7 @@ window.__ModuleLoader__.load({
181
276
  : snapshot.status === 'unavailable'
182
277
  ? h('p', { className: 'ptcPlusMessage' }, t('state.unavailable'))
183
278
  : [
184
- ...CONFIG_FIELDS.map(field => h('div', { key: field.key, className: 'ptcPlusRow' },
185
- h('div', { className: 'ptcPlusMain' },
186
- h('div', { className: 'ptcPlusLabel' }, t(`${field.key}.label`)),
187
- field.description === '' ? null : h('div', { className: 'ptcPlusDetail' }, t(`${field.key}.description`))),
188
- fieldInput(field, value[field.key], fieldDisabled(field), persist, t(`${field.key}.label`)))),
279
+ ...settingGroups,
189
280
  h('div', { key: 'footer', className: 'ptcPlusFooter' },
190
281
  h('span', { className: 'ptcPlusMessage', role: 'status' }, status === null
191
282
  ? t(snapshot.writable ? 'footer.live' : 'footer.readOnly')
@@ -198,22 +289,367 @@ window.__ModuleLoader__.load({
198
289
  name: 'settings.plugin.item', key: SETTINGS_NAMESPACE, locale: LOCALE_NS,
199
290
  }, PTCPlusSettingsCard))
200
291
 
292
+ function PTCPlusToolRow({ toolName, block, inspect, t }) {
293
+ const [open, setOpen] = React.useState(false)
294
+ const view = derivePtcToolView(block, toolName)
295
+ const expandable = view.code !== '' || view.output !== '' || typeof inspect === 'function'
296
+ const stateKey = {
297
+ running: 'tool.running',
298
+ ok: 'tool.completed',
299
+ error: 'tool.failed',
300
+ stopped: 'tool.stopped',
301
+ }[view.state]
302
+ const outputSummary = view.state === 'error' && view.output !== ''
303
+ ? view.output.split(/\r?\n/, 1)[0]
304
+ : ''
305
+ const summary = outputSummary || view.description
306
+ const stateText = view.state === 'ok' ? null : t(stateKey)
307
+ const toggle = () => {
308
+ if (expandable) setOpen(current => !current)
309
+ }
310
+ const summaryText = summary === '' ? null : summary
311
+ const summaryLine = stateText === null && summaryText === null ? null
312
+ : h('div', { className: 'ptcPlusToolSummaryLine', 'data-state': view.state, role: 'status' },
313
+ stateText === null ? null : h('span', { className: 'ptcPlusToolState' }, stateText),
314
+ summaryText === null ? null : h('span', { className: 'ptcPlusToolSep', 'aria-hidden': true }),
315
+ summaryText === null ? null : h('span', { className: 'ptcPlusToolDescription' }, summaryText))
316
+ const body = !open ? null : h('div', { className: 'ptcPlusToolBody' },
317
+ view.code === '' ? null : h('div', { className: 'ptcPlusToolSection' },
318
+ h('span', { className: 'ptcPlusToolSectionLabel' }, t('tool.source')),
319
+ typeof CodeBlock === 'function'
320
+ ? h(CodeBlock, {
321
+ code: view.code, lang: 'typescript', className: 'ptcPlusToolCode',
322
+ copyLabel: t('tool.copy'), copiedLabel: t('tool.copied'),
323
+ })
324
+ : h('pre', { className: 'ptcPlusToolCode' }, view.code)),
325
+ view.output === '' ? null : h('div', { className: 'ptcPlusToolSection' },
326
+ h('span', { className: 'ptcPlusToolSectionLabel' }, t('tool.result')),
327
+ h('div', { className: 'ptcPlusIoCard' },
328
+ h('pre', {
329
+ className: 'ptcPlusIoText',
330
+ 'data-error': view.state === 'error' || undefined,
331
+ }, view.output))),
332
+ typeof inspect !== 'function' ? null : h('button', {
333
+ type: 'button', className: 'ptcPlusInspect', onClick: inspect,
334
+ }, h(IconInspectOutline12, { 'aria-hidden': true }), t('tool.inspect')))
335
+ const features = view.features.length === 0 ? null : h('div', { className: 'ptcPlusFeatures' },
336
+ view.features.map(feature => h('span', {
337
+ key: `${feature.key}:${feature.detail}`, className: 'ptcPlusFeature',
338
+ },
339
+ h('span', { className: 'ptcPlusFeatureName' }, t(feature.key)),
340
+ feature.detail === '' ? null
341
+ : h('span', { className: 'ptcPlusFeatureDetail', title: feature.detail }, feature.detail))))
342
+ const collapsedContent = h('div', { className: 'ptcPlusToolPreview' }, summaryLine, features)
343
+ if (typeof DisclosureRow === 'function') {
344
+ return h('div', { className: 'ptcPlusTool' },
345
+ h(DisclosureRow, {
346
+ icon: expandable
347
+ ? h(IconChevronDownOutline14, { size: 14 })
348
+ : h(IconCheckOutline14, { size: 14 }),
349
+ title: t(toolName === 'edit_run_code' ? 'tool.codeEdit' : 'tool.code'),
350
+ open,
351
+ expandable,
352
+ onToggle: toggle,
353
+ expandOnRowClick: true,
354
+ previewChevron: false,
355
+ keepContentWhenOpen: true,
356
+ collapsedContent,
357
+ children: body,
358
+ }))
359
+ }
360
+ return h('div', { className: 'ptcPlusTool' },
361
+ h('div', {
362
+ className: 'ptcPlusToolSummary', 'data-state': view.state,
363
+ 'data-expandable': expandable || undefined,
364
+ role: expandable ? 'button' : undefined,
365
+ tabIndex: expandable ? 0 : undefined,
366
+ 'aria-expanded': expandable ? open : undefined,
367
+ onClick: expandable ? toggle : undefined,
368
+ onKeyDown: expandable ? (event) => {
369
+ if (event.key !== 'Enter' && event.key !== ' ') return
370
+ event.preventDefault()
371
+ toggle()
372
+ } : undefined,
373
+ },
374
+ h('span', { className: 'ptcPlusToolLeading', 'aria-hidden': true }, expandable
375
+ ? h(IconChevronDownOutline14, { size: 14, className: 'ptcPlusToolChevron', 'data-open': open })
376
+ : h(IconCheckOutline14, { size: 14 })),
377
+ h('span', { className: 'ptcPlusToolTitle' }, t(
378
+ toolName === 'edit_run_code' ? 'tool.codeEdit' : 'tool.code')),
379
+ view.state === 'ok' ? null
380
+ : h('span', { className: 'ptcPlusToolState', role: 'status' }, t(stateKey)),
381
+ h('span', { className: 'ptcPlusToolSep', 'aria-hidden': true }),
382
+ h('span', { className: 'ptcPlusToolDescription' }, summary)),
383
+ features,
384
+ body)
385
+ }
386
+
387
+ ctx.slots.inject('tool.call.toolview', () => {
388
+ let releaseRows
389
+ const release = () => {
390
+ releaseRows?.()
391
+ releaseRows = undefined
392
+ }
393
+ const sync = () => {
394
+ const snapshot = preferenceScope.getSnapshot()
395
+ const enabled = snapshot.status === 'ready'
396
+ && snapshot.value?.enabled === true
397
+ && snapshot.value?.enhancedToolView !== false
398
+ if (enabled === (releaseRows !== undefined)) return
399
+ release()
400
+ if (!enabled) return
401
+ const disposers = []
402
+ try {
403
+ disposers.push(ctx.slots.register({
404
+ name: 'tool.call.toolview', key: 'run_code', locale: LOCALE_NS,
405
+ }, PTCPlusToolRow))
406
+ disposers.push(ctx.slots.register({
407
+ name: 'tool.call.toolview', key: 'edit_run_code', locale: LOCALE_NS,
408
+ }, PTCPlusToolRow))
409
+ } catch (error) {
410
+ disposers.reverse().forEach(dispose => dispose())
411
+ throw error
412
+ }
413
+ releaseRows = () => disposers.reverse().forEach(dispose => dispose())
414
+ }
415
+ sync()
416
+ const unsubscribe = preferenceScope.subscribe(sync)
417
+ return () => {
418
+ unsubscribe()
419
+ release()
420
+ }
421
+ })
422
+
201
423
  ctx.inject(['slots', 'sessions'], (scope) => {
202
- function PTCPlusSessionIndicator({ sessionId, t }) {
203
- const sessions = React.useSyncExternalStore(
204
- listener => scope.sessions.list.subscribe(listener),
205
- () => scope.sessions.list.getSnapshot(),
206
- () => scope.sessions.list.getSnapshot(),
424
+ function replPopoverIsOpen(popover) {
425
+ if (popover?.dataset?.open === 'true') return true
426
+ try {
427
+ return popover?.matches?.(':popover-open') === true
428
+ } catch {
429
+ return false
430
+ }
431
+ }
432
+
433
+ function placeReplPopover(trigger, popover) {
434
+ if (trigger === null || popover === null) return
435
+ const margin = 12
436
+ const gap = 8
437
+ const viewportWidth = document.documentElement.clientWidth || window.innerWidth
438
+ const viewportHeight = document.documentElement.clientHeight || window.innerHeight
439
+ const triggerRect = trigger.getBoundingClientRect()
440
+ const width = Math.min(344, Math.max(0, viewportWidth - margin * 2))
441
+ const left = Math.min(
442
+ Math.max(margin, triggerRect.right - width),
443
+ Math.max(margin, viewportWidth - width - margin),
207
444
  )
445
+ const below = Math.max(0, viewportHeight - triggerRect.bottom - gap - margin)
446
+ const above = Math.max(0, triggerRect.top - gap - margin)
447
+ const opensAbove = below < 260 && above > below
448
+ const availableHeight = Math.max(80, opensAbove ? above : below)
449
+ popover.style.width = `${width}px`
450
+ popover.style.maxHeight = `${availableHeight}px`
451
+ popover.style.left = `${left}px`
452
+ popover.style.top = opensAbove
453
+ ? `${Math.max(margin, triggerRect.top - gap - Math.min(popover.offsetHeight, availableHeight))}px`
454
+ : `${Math.min(viewportHeight - margin, triggerRect.bottom + gap)}px`
455
+ }
456
+
457
+ function ReplMemoryCard({ memory, t, id, titleId, popoverRef, onEnter, onLeave }) {
458
+ const [expandedBinding, setExpandedBinding] = React.useState(null)
459
+ return h('div', {
460
+ className: 'ptcPlusReplPopover', id, ref: popoverRef, popover: 'auto',
461
+ role: 'dialog', 'aria-labelledby': titleId,
462
+ onPointerEnter: onEnter, onPointerLeave: onLeave,
463
+ },
464
+ h('div', { className: 'ptcPlusReplCard' },
465
+ h('div', { className: 'ptcPlusReplHead' },
466
+ h('span', { className: 'ptcPlusReplStatusDot', 'aria-hidden': true }),
467
+ h('span', { className: 'ptcPlusReplTitle', id: titleId }, t('memory.title')),
468
+ memory.available
469
+ ? h('span', { className: 'ptcPlusReplSummary' }, t('memory.count', { count: memory.total }))
470
+ : null),
471
+ !memory.available
472
+ ? h('span', { className: 'ptcPlusReplEmpty' }, t('memory.unavailable'))
473
+ : memory.entries.length === 0
474
+ ? h('span', { className: 'ptcPlusReplEmpty' }, t('memory.empty'))
475
+ : h('ul', { className: 'ptcPlusReplList' }, memory.entries.map((binding, index) => {
476
+ const expanded = expandedBinding === binding.name
477
+ const preview = binding.definition.source.replace(/\s+/g, ' ').trim()
478
+ const definitionId = `${id}-binding-${index}`
479
+ const toggle = () => setExpandedBinding(current => (
480
+ current === binding.name ? null : binding.name
481
+ ))
482
+ return h('li', {
483
+ className: 'ptcPlusReplBinding', key: binding.name,
484
+ 'data-expanded': expanded,
485
+ },
486
+ h('button', {
487
+ className: 'ptcPlusReplBindingTrigger', type: 'button',
488
+ 'aria-expanded': expanded,
489
+ 'aria-controls': expanded ? definitionId : undefined,
490
+ onClick: toggle,
491
+ },
492
+ h('span', {
493
+ className: 'ptcPlusReplName', 'data-kind': binding.kind,
494
+ title: `${binding.name} - ${t(`memory.kind.${binding.kind}`)}`,
495
+ }, binding.name),
496
+ h('span', { className: 'ptcPlusReplPreview', title: preview }, preview),
497
+ h('span', {
498
+ className: 'ptcPlusReplChevron', 'data-open': expanded, 'aria-hidden': true,
499
+ }, h(IconChevronDownOutline14, { size: 14 }))),
500
+ expanded
501
+ ? h('div', {
502
+ className: 'ptcPlusReplDefinitionWrap', 'data-open': true,
503
+ id: definitionId, role: 'region', 'aria-label': binding.name,
504
+ }, h('div', { className: 'ptcPlusReplDefinitionInner' },
505
+ h('div', { className: 'ptcPlusReplDefinition' },
506
+ h('span', { className: 'ptcPlusReplLocation' }, t('memory.location', {
507
+ line: binding.definition.line,
508
+ column: binding.definition.column,
509
+ })),
510
+ typeof CodeBlock === 'function'
511
+ ? h(CodeBlock, {
512
+ code: binding.definition.source,
513
+ lang: 'typescript',
514
+ className: 'ptcPlusReplCode',
515
+ copyLabel: t('tool.copy'),
516
+ copiedLabel: t('tool.copied'),
517
+ })
518
+ : h('pre', { className: 'ptcPlusReplCode' }, binding.definition.source))))
519
+ : null)
520
+ })),
521
+ memory.omitted === 0 ? null
522
+ : h('span', { className: 'ptcPlusReplMore' }, t('memory.more', { count: memory.omitted }))))
523
+ }
524
+
525
+ function PTCPlusSessionIndicator({ sessionId, t, useSession, useProjection, useSessions }) {
526
+ // The session-scoped standard kit changed between DSH lines. Prefer its
527
+ // public hooks when present, then fall back to the older sessions list.
528
+ let conversation
529
+ try {
530
+ conversation = typeof useSession === 'function'
531
+ ? useSession(snapshot => snapshot)
532
+ : undefined
533
+ } catch {
534
+ conversation = undefined
535
+ }
536
+ let projectionMemory
537
+ try {
538
+ projectionMemory = typeof useProjection === 'function'
539
+ ? useProjection('ptcPlusRepl')
540
+ : undefined
541
+ } catch {
542
+ projectionMemory = undefined
543
+ }
544
+ let sessions
545
+ try {
546
+ sessions = typeof useSessions === 'function'
547
+ ? useSessions(snapshot => snapshot)
548
+ : React.useSyncExternalStore(
549
+ listener => scope.sessions?.list?.subscribe?.(listener) ?? (() => {}),
550
+ () => scope.sessions?.list?.getSnapshot?.() ?? {},
551
+ () => scope.sessions?.list?.getSnapshot?.() ?? {},
552
+ )
553
+ } catch {
554
+ sessions = {}
555
+ }
208
556
  const settings = React.useSyncExternalStore(
209
557
  listener => preferenceScope.subscribe(listener),
210
558
  () => preferenceScope.getSnapshot(),
211
559
  () => preferenceScope.getSnapshot(),
212
560
  )
213
- if (!sessionUsesPtcPreset(sessions.byId?.[sessionId])
561
+ const triggerRef = React.useRef(null)
562
+ const popoverRef = React.useRef(null)
563
+ const closeTimer = React.useRef(undefined)
564
+ const [expanded, setExpanded] = React.useState(false)
565
+ const positionPopover = React.useCallback(() => {
566
+ if (!replPopoverIsOpen(popoverRef.current)) return
567
+ placeReplPopover(triggerRef.current, popoverRef.current)
568
+ }, [])
569
+ const showPopover = React.useCallback(() => {
570
+ if (closeTimer.current !== undefined) clearTimeout(closeTimer.current)
571
+ const popover = popoverRef.current
572
+ if (popover === null) return
573
+ popover.style.visibility = 'hidden'
574
+ if (!replPopoverIsOpen(popover)) {
575
+ if (typeof popover.showPopover === 'function') {
576
+ try {
577
+ popover.showPopover()
578
+ } catch {
579
+ popover.dataset.open = 'true'
580
+ }
581
+ } else {
582
+ popover.dataset.open = 'true'
583
+ }
584
+ }
585
+ placeReplPopover(triggerRef.current, popover)
586
+ popover.style.visibility = 'visible'
587
+ setExpanded(true)
588
+ }, [])
589
+ const hidePopover = React.useCallback(() => {
590
+ const popover = popoverRef.current
591
+ if (popover === null) return
592
+ if (popover.dataset.open === 'true') delete popover.dataset.open
593
+ if (typeof popover.hidePopover === 'function' && replPopoverIsOpen(popover)) {
594
+ try { popover.hidePopover() } catch {}
595
+ }
596
+ setExpanded(false)
597
+ }, [])
598
+ const scheduleHide = React.useCallback(() => {
599
+ if (closeTimer.current !== undefined) clearTimeout(closeTimer.current)
600
+ closeTimer.current = setTimeout(() => {
601
+ closeTimer.current = undefined
602
+ if (document.activeElement === triggerRef.current
603
+ || popoverRef.current?.contains(document.activeElement)) return
604
+ hidePopover()
605
+ }, 120)
606
+ }, [hidePopover])
607
+ React.useEffect(() => {
608
+ const syncPopoverState = (event) => {
609
+ if (event.target !== popoverRef.current) return
610
+ setExpanded(replPopoverIsOpen(popoverRef.current))
611
+ }
612
+ window.addEventListener('resize', positionPopover)
613
+ document.addEventListener('scroll', positionPopover, true)
614
+ document.addEventListener('toggle', syncPopoverState, true)
615
+ return () => {
616
+ if (closeTimer.current !== undefined) clearTimeout(closeTimer.current)
617
+ window.removeEventListener('resize', positionPopover)
618
+ document.removeEventListener('scroll', positionPopover, true)
619
+ document.removeEventListener('toggle', syncPopoverState, true)
620
+ const popover = popoverRef.current
621
+ if (popover?.dataset?.open === 'true') delete popover.dataset.open
622
+ if (typeof popover?.hidePopover === 'function' && replPopoverIsOpen(popover)) {
623
+ try { popover.hidePopover() } catch {}
624
+ }
625
+ }
626
+ }, [hidePopover, positionPopover])
627
+ const resolvedSessionId = sessionId ?? conversation?.sessionId
628
+ const session = sessions?.byId?.[resolvedSessionId] ?? conversation
629
+ if (!sessionUsesPtcPreset(session)
214
630
  || settings.status !== 'ready' || settings.value?.enabled !== true) return null
215
- return h('span', { className: 'ptcPlusActive', title: t('indicator.title') },
216
- h(IconCheckOutline14, { size: 14 }), 'PTC Plus')
631
+ let memory
632
+ try {
633
+ memory = normalizeReplMemorySnapshot(projectionMemory
634
+ ?? session?.projectionValues?.ptcPlusRepl)
635
+ } catch {
636
+ memory = unavailableReplMemorySnapshot()
637
+ }
638
+ const popoverId = `ptc-plus-repl-${String(resolvedSessionId).replace(/[^A-Za-z0-9_-]/g, '-')}`
639
+ const titleId = `${popoverId}-title`
640
+ return h('span', { className: 'ptcPlusActiveShell' },
641
+ h('button', {
642
+ type: 'button', className: 'ptcPlusActive', ref: triggerRef,
643
+ 'aria-label': t('indicator.title'), 'aria-controls': popoverId,
644
+ 'aria-expanded': expanded, 'aria-haspopup': 'dialog',
645
+ onPointerEnter: showPopover, onPointerLeave: scheduleHide,
646
+ onFocus: showPopover, onBlur: scheduleHide, onClick: showPopover,
647
+ onKeyDown: event => { if (event.key === 'Escape') hidePopover() },
648
+ }, h('span', { className: 'ptcPlusActiveLabel' }, 'PTC Plus')),
649
+ h(ReplMemoryCard, {
650
+ memory, t, id: popoverId, titleId, popoverRef,
651
+ onEnter: showPopover, onLeave: scheduleHide,
652
+ }))
217
653
  }
218
654
  scope.slots.inject('conversation.session.header.actions', () => scope.slots.register({
219
655
  name: 'conversation.session.header.actions', id: 'ptc-plus-active', order: -9, locale: LOCALE_NS,