@norman-else/dsh-claude 0.1.34 → 0.1.35
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.
- package/README.md +5 -1
- package/lib/bin.mjs +1 -1
- package/lib/client.d.ts +4 -0
- package/lib/client.js +1160 -614
- package/lib/client.js.map +1 -1
- package/lib/{events-DeSV0S1-.mjs → events-Doid-tq7.mjs} +8 -2
- package/lib/events-Doid-tq7.mjs.map +1 -0
- package/lib/index.d.mts +54 -2
- package/lib/index.mjs +1185 -480
- package/lib/index.mjs.map +1 -1
- package/lib/presenters-CXtE8qve.mjs +300 -0
- package/lib/presenters-CXtE8qve.mjs.map +1 -0
- package/lib/{preset-installer-BY4846KK.mjs → preset-installer-BMyKr5eQ.mjs} +2 -2
- package/lib/{preset-installer-BY4846KK.mjs.map → preset-installer-BMyKr5eQ.mjs.map} +1 -1
- package/lib/preset-route.mjs +2 -172
- package/lib/preset-route.mjs.map +1 -1
- package/package.json +189 -188
- package/lib/command-bridge-DXI6nWhB.mjs +0 -71
- package/lib/command-bridge-DXI6nWhB.mjs.map +0 -1
- package/lib/events-DeSV0S1-.mjs.map +0 -1
package/lib/client.js
CHANGED
|
@@ -27,6 +27,9 @@ window.__ModuleLoader__.load({
|
|
|
27
27
|
const CLAUDE_ASK_PATH = "/plugins/dsh-claude/ask";
|
|
28
28
|
const CLAUDE_EDITOR_OPEN_PATH = "/plugins/dsh-claude/editor/open";
|
|
29
29
|
const CLAUDE_REWIND_PATH = "/plugins/dsh-claude/rewind";
|
|
30
|
+
function isClaudeRenderMode(value) {
|
|
31
|
+
return value === "plugin" || value === "native";
|
|
32
|
+
}
|
|
30
33
|
//#endregion
|
|
31
34
|
//#region src/client/task-projection.ts
|
|
32
35
|
/** Tasks UI is reserved for detached work and genuine Claude subagents. */
|
|
@@ -239,7 +242,7 @@ window.__ModuleLoader__.load({
|
|
|
239
242
|
case "WebFetch": {
|
|
240
243
|
const url = inputString(input, "url") ?? "a web page";
|
|
241
244
|
completed = `Fetched ${url}`;
|
|
242
|
-
failedAction = `
|
|
245
|
+
failedAction = `load ${url}`;
|
|
243
246
|
break;
|
|
244
247
|
}
|
|
245
248
|
case "WebSearch":
|
|
@@ -267,8 +270,20 @@ window.__ModuleLoader__.load({
|
|
|
267
270
|
}
|
|
268
271
|
return failed ? `Failed to ${failedAction}` : completed;
|
|
269
272
|
}
|
|
273
|
+
/** Whether the Host drew this step with DSH's own renderer.
|
|
274
|
+
*
|
|
275
|
+
* The stamp rides on the records themselves rather than on a Client-side copy
|
|
276
|
+
* of the setting, because the two can disagree: the Host switches on the next
|
|
277
|
+
* turn while a running Client keeps whatever it decided at boot, and the
|
|
278
|
+
* failure mode of disagreeing is drawing every step twice. Reading it back per
|
|
279
|
+
* step also keeps history honest in both directions -- a turn recorded under
|
|
280
|
+
* one renderer keeps it after the setting changes. */
|
|
281
|
+
function nativelyRenderedStep(activities, turn, step) {
|
|
282
|
+
return activities.some((activity) => activity.turn === turn && activity.step === step && activity.renderer === "native");
|
|
283
|
+
}
|
|
270
284
|
/** Fold one step's shared ordinal stream into Claude Code-style prose and tool groups. */
|
|
271
285
|
function transcriptItemsForStep(activities, turn, step, tasks = []) {
|
|
286
|
+
if (nativelyRenderedStep(activities, turn, step)) return [];
|
|
272
287
|
const ordered = activities.filter((activity) => activity.turn === turn && activity.step === step && isProjectedTaskActivity(activity, tasks)).slice().sort((left, right) => left.ordinal - right.ordinal);
|
|
273
288
|
const rows = foldedRows(ordered, tasks);
|
|
274
289
|
const placed = /* @__PURE__ */ new Set();
|
|
@@ -674,30 +689,65 @@ window.__ModuleLoader__.load({
|
|
|
674
689
|
fontSize: 14,
|
|
675
690
|
lineHeight: "22px"
|
|
676
691
|
};
|
|
677
|
-
const
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
692
|
+
const settingSelectTriggerClass = "dshClaudeSettingSelectTrigger";
|
|
693
|
+
const settingSelectChevronClass = "dshClaudeSettingSelectChevron";
|
|
694
|
+
/**
|
|
695
|
+
* The listbox trigger and its chevron, in a stylesheet rather than inline
|
|
696
|
+
* because the states it has to draw cannot be expressed inline.
|
|
697
|
+
*
|
|
698
|
+
* Focus is the reason. An inline style carries no pseudo-class, so the blue
|
|
699
|
+
* ring used to ride on the open state alone: choosing an option closed the
|
|
700
|
+
* menu and took the ring with it while the button kept DOM focus, leaving the
|
|
701
|
+
* UA's own focus ring -- white on a dark theme, drawn outside the radius --
|
|
702
|
+
* as the only indicator. Focus and open are separate states; both draw the
|
|
703
|
+
* ring here, and `outline: none` retires the UA's.
|
|
704
|
+
*
|
|
705
|
+
* The open state reads `aria-expanded` instead of a second class because the
|
|
706
|
+
* attribute is already on the element and already correct. Its value is left
|
|
707
|
+
* unquoted: a valid identifier needs no quotes, and the sheet then survives
|
|
708
|
+
* React's server escaping, which would otherwise write `"` into markup a
|
|
709
|
+
* browser reads as raw text.
|
|
710
|
+
*/
|
|
711
|
+
const settingSelectCss = `
|
|
712
|
+
.${settingSelectTriggerClass} {
|
|
713
|
+
width: 100%;
|
|
714
|
+
min-height: 38px;
|
|
715
|
+
display: flex;
|
|
716
|
+
align-items: center;
|
|
717
|
+
justify-content: space-between;
|
|
718
|
+
gap: 12px;
|
|
719
|
+
padding: 7px 11px 7px 13px;
|
|
720
|
+
border: 1px solid var(--dsw-alias-border-l2, color-mix(in srgb, currentColor 16%, transparent));
|
|
721
|
+
border-radius: 10px;
|
|
722
|
+
background: var(--dsw-alias-bg-layer-2);
|
|
723
|
+
color: var(--dsw-alias-label-primary);
|
|
724
|
+
box-shadow: 0 1px 2px color-mix(in srgb, var(--dsw-alias-label-primary) 5%, transparent);
|
|
725
|
+
font: inherit;
|
|
726
|
+
font-size: 14px;
|
|
727
|
+
line-height: 22px;
|
|
728
|
+
text-align: left;
|
|
729
|
+
cursor: pointer;
|
|
730
|
+
transition: border-color 120ms ease, box-shadow 120ms ease, background 120ms ease;
|
|
731
|
+
}
|
|
732
|
+
.${settingSelectTriggerClass}:focus-visible,
|
|
733
|
+
.${settingSelectTriggerClass}[aria-expanded=true] {
|
|
734
|
+
outline: none;
|
|
735
|
+
border-color: var(--dsw-static-blue-450);
|
|
736
|
+
box-shadow: 0 0 0 3px color-mix(in srgb, var(--dsw-static-blue-450) 15%, transparent);
|
|
737
|
+
}
|
|
738
|
+
.${settingSelectChevronClass} {
|
|
739
|
+
flex: none;
|
|
740
|
+
display: grid;
|
|
741
|
+
place-items: center;
|
|
742
|
+
width: 16px;
|
|
743
|
+
height: 16px;
|
|
744
|
+
color: var(--dsw-alias-label-tertiary);
|
|
745
|
+
transition: transform 120ms ease;
|
|
746
|
+
}
|
|
747
|
+
.${settingSelectTriggerClass}[aria-expanded=true] .${settingSelectChevronClass} {
|
|
748
|
+
transform: rotate(180deg);
|
|
749
|
+
}
|
|
750
|
+
`;
|
|
701
751
|
const settingSelectValue = {
|
|
702
752
|
minWidth: 0,
|
|
703
753
|
flex: 1,
|
|
@@ -705,15 +755,6 @@ window.__ModuleLoader__.load({
|
|
|
705
755
|
textOverflow: "ellipsis",
|
|
706
756
|
whiteSpace: "nowrap"
|
|
707
757
|
};
|
|
708
|
-
const settingSelectChevron = {
|
|
709
|
-
flex: "none",
|
|
710
|
-
color: "var(--dsw-alias-label-tertiary)",
|
|
711
|
-
fontSize: 16,
|
|
712
|
-
lineHeight: 1,
|
|
713
|
-
transform: "translateY(-1px)",
|
|
714
|
-
transition: "transform 120ms ease"
|
|
715
|
-
};
|
|
716
|
-
const settingSelectChevronOpen = { transform: "translateY(1px) rotate(180deg)" };
|
|
717
758
|
const settingSelectMenu = {
|
|
718
759
|
position: "absolute",
|
|
719
760
|
zIndex: 20,
|
|
@@ -3736,6 +3777,203 @@ window.__ModuleLoader__.load({
|
|
|
3736
3777
|
});
|
|
3737
3778
|
}
|
|
3738
3779
|
//#endregion
|
|
3780
|
+
//#region src/client/markdown-theme.ts
|
|
3781
|
+
/** Claude Code's code presentation over the Host's Markdown renderer.
|
|
3782
|
+
*
|
|
3783
|
+
* This package renders no Markdown of its own — prose, fenced blocks and the
|
|
3784
|
+
* copy affordance all come from the Host's `MarkdownText` primitive. What it
|
|
3785
|
+
* can own is what that primitive reads: the palette (custom properties) and
|
|
3786
|
+
* the block's chrome (CSS over the primitive's own markup).
|
|
3787
|
+
*
|
|
3788
|
+
* PARITY IS PARTIAL BY CONSTRUCTION. Both renderers highlight with shiki, but
|
|
3789
|
+
* Claude Code's desktop build loads a full TextMate theme (Pierre Dark /
|
|
3790
|
+
* Pierre Light Soft: 248 tokenColor rules over 424 scopes) and bakes the
|
|
3791
|
+
* resolved colour into every span, while the Host loads shiki's legacy
|
|
3792
|
+
* `css-variables` theme, which collapses every scope into the eleven buckets
|
|
3793
|
+
* below. Those eleven carry the colours that dominate a code block —
|
|
3794
|
+
* keywords, strings, comments, functions, numbers — and nothing here can
|
|
3795
|
+
* recover the rest: `constant.numeric` and `constant` are two different
|
|
3796
|
+
* colours in Pierre and one bucket here, and Pierre's string-coloured string
|
|
3797
|
+
* delimiters share this sheet's single punctuation bucket. Matching the rest
|
|
3798
|
+
* means this package running its own shiki, which is a different decision
|
|
3799
|
+
* with a different cost.
|
|
3800
|
+
*
|
|
3801
|
+
* Every rule fails open the way `host-chrome` does: the palette is
|
|
3802
|
+
* declarations on this package's own wrapper, and the chrome rules match the
|
|
3803
|
+
* primitive's CSS Module local names, so a Host that renames either simply
|
|
3804
|
+
* stops matching and its stock presentation comes back.
|
|
3805
|
+
*/
|
|
3806
|
+
/** Wrapper class carrying the palette and scoping the chrome rules.
|
|
3807
|
+
* `display:contents` so the extra element generates no box — custom
|
|
3808
|
+
* properties inherit through it regardless. */
|
|
3809
|
+
const CLAUDE_MARKDOWN_SCOPE = "dsh-claude-markdown";
|
|
3810
|
+
/** Pierre mapped onto the Host's eleven buckets. Each entry names the theme
|
|
3811
|
+
* scope the value was taken from, because the mapping — not the colour — is
|
|
3812
|
+
* the part a reader has to check. */
|
|
3813
|
+
const PIERRE_DARK = [
|
|
3814
|
+
[
|
|
3815
|
+
"--shiki-background",
|
|
3816
|
+
"#1a1a19",
|
|
3817
|
+
"UI surface (--cds-surface-2)"
|
|
3818
|
+
],
|
|
3819
|
+
[
|
|
3820
|
+
"--shiki-foreground",
|
|
3821
|
+
"#fafafa",
|
|
3822
|
+
"editor.foreground"
|
|
3823
|
+
],
|
|
3824
|
+
[
|
|
3825
|
+
"--shiki-token-comment",
|
|
3826
|
+
"#737373",
|
|
3827
|
+
"comment"
|
|
3828
|
+
],
|
|
3829
|
+
[
|
|
3830
|
+
"--shiki-token-keyword",
|
|
3831
|
+
"#ff678d",
|
|
3832
|
+
"keyword, storage.type"
|
|
3833
|
+
],
|
|
3834
|
+
[
|
|
3835
|
+
"--shiki-token-string",
|
|
3836
|
+
"#5ecc71",
|
|
3837
|
+
"string"
|
|
3838
|
+
],
|
|
3839
|
+
[
|
|
3840
|
+
"--shiki-token-string-expression",
|
|
3841
|
+
"#ffa359",
|
|
3842
|
+
"punctuation.section.embedded"
|
|
3843
|
+
],
|
|
3844
|
+
[
|
|
3845
|
+
"--shiki-token-function",
|
|
3846
|
+
"#9d6afb",
|
|
3847
|
+
"entity.name.function"
|
|
3848
|
+
],
|
|
3849
|
+
[
|
|
3850
|
+
"--shiki-token-constant",
|
|
3851
|
+
"#68cdf2",
|
|
3852
|
+
"constant.numeric, constant.language"
|
|
3853
|
+
],
|
|
3854
|
+
[
|
|
3855
|
+
"--shiki-token-parameter",
|
|
3856
|
+
"#a3a3a3",
|
|
3857
|
+
"variable.parameter"
|
|
3858
|
+
],
|
|
3859
|
+
[
|
|
3860
|
+
"--shiki-token-punctuation",
|
|
3861
|
+
"#636363",
|
|
3862
|
+
"punctuation"
|
|
3863
|
+
],
|
|
3864
|
+
[
|
|
3865
|
+
"--shiki-token-link",
|
|
3866
|
+
"#ff678d",
|
|
3867
|
+
"markup.underline.link.markdown"
|
|
3868
|
+
]
|
|
3869
|
+
];
|
|
3870
|
+
const PIERRE_LIGHT = [
|
|
3871
|
+
[
|
|
3872
|
+
"--shiki-background",
|
|
3873
|
+
"#ffffff",
|
|
3874
|
+
"UI surface (--cds-surface-2)"
|
|
3875
|
+
],
|
|
3876
|
+
[
|
|
3877
|
+
"--shiki-foreground",
|
|
3878
|
+
"#525252",
|
|
3879
|
+
"editor.foreground"
|
|
3880
|
+
],
|
|
3881
|
+
[
|
|
3882
|
+
"--shiki-token-comment",
|
|
3883
|
+
"#8a8a8a",
|
|
3884
|
+
"comment"
|
|
3885
|
+
],
|
|
3886
|
+
[
|
|
3887
|
+
"--shiki-token-keyword",
|
|
3888
|
+
"#ff678d",
|
|
3889
|
+
"keyword, storage.type"
|
|
3890
|
+
],
|
|
3891
|
+
[
|
|
3892
|
+
"--shiki-token-string",
|
|
3893
|
+
"#0dbe4e",
|
|
3894
|
+
"string"
|
|
3895
|
+
],
|
|
3896
|
+
[
|
|
3897
|
+
"--shiki-token-string-expression",
|
|
3898
|
+
"#fe8c2c",
|
|
3899
|
+
"punctuation.section.embedded"
|
|
3900
|
+
],
|
|
3901
|
+
[
|
|
3902
|
+
"--shiki-token-function",
|
|
3903
|
+
"#9d6afb",
|
|
3904
|
+
"entity.name.function"
|
|
3905
|
+
],
|
|
3906
|
+
[
|
|
3907
|
+
"--shiki-token-constant",
|
|
3908
|
+
"#08c0ef",
|
|
3909
|
+
"constant.numeric, constant.language"
|
|
3910
|
+
],
|
|
3911
|
+
[
|
|
3912
|
+
"--shiki-token-parameter",
|
|
3913
|
+
"#737373",
|
|
3914
|
+
"variable.parameter"
|
|
3915
|
+
],
|
|
3916
|
+
[
|
|
3917
|
+
"--shiki-token-punctuation",
|
|
3918
|
+
"#737373",
|
|
3919
|
+
"punctuation"
|
|
3920
|
+
],
|
|
3921
|
+
[
|
|
3922
|
+
"--shiki-token-link",
|
|
3923
|
+
"#ff678d",
|
|
3924
|
+
"markup.underline.link.markdown"
|
|
3925
|
+
]
|
|
3926
|
+
];
|
|
3927
|
+
/** Claude's brand clay (`--cds-hsl-clay`), its inline-code TEXT colour; the
|
|
3928
|
+
* emphasized ramp is the light-theme variant. The chip's fill is deliberately
|
|
3929
|
+
* NOT tinted with it — see {@link INLINE_FILL_DARK}. */
|
|
3930
|
+
const CLAY = "#d97757";
|
|
3931
|
+
const CLAY_EMPHASIZED = "#c8603f";
|
|
3932
|
+
/** The inline-code chip fill: a neutral 4% wash (Claude's `--t1`), not a tint
|
|
3933
|
+
* of the text colour. A clay-tinted fill reads as a coloured box around every
|
|
3934
|
+
* identifier; the neutral one disappears into the surface and lets the text
|
|
3935
|
+
* carry the accent, which is what the chip is for. */
|
|
3936
|
+
const INLINE_FILL_DARK = "hsl(0 0% 100% / .04)";
|
|
3937
|
+
const INLINE_FILL_LIGHT = "hsl(0 0% 4.3% / .04)";
|
|
3938
|
+
/** Block corner radius (Claude's `--r6`), against the Host's own 12px. */
|
|
3939
|
+
const BLOCK_RADIUS = "8px";
|
|
3940
|
+
/** @param banner - equal to `surface` on purpose: the bar is floated out of
|
|
3941
|
+
* the way below, so this colour is only reached if those chrome selectors
|
|
3942
|
+
* miss, and a bar that matches the block is the neutral fallback. */
|
|
3943
|
+
function palette(entries, surface, banner, inlineFill) {
|
|
3944
|
+
return [
|
|
3945
|
+
...entries.map(([name, value]) => `${name}:${value};`),
|
|
3946
|
+
`--dsw-alias-markdown-code-block:${surface};`,
|
|
3947
|
+
`--dsw-alias-markdown-code-block-banner:${banner};`,
|
|
3948
|
+
`--dsw-alias-markdown-inline-code:${inlineFill}`
|
|
3949
|
+
].join("");
|
|
3950
|
+
}
|
|
3951
|
+
const CLAUDE_MARKDOWN_THEME_CSS = [
|
|
3952
|
+
`.${CLAUDE_MARKDOWN_SCOPE}{display:contents;`,
|
|
3953
|
+
palette(PIERRE_LIGHT, "#ffffff", "#ffffff", INLINE_FILL_LIGHT),
|
|
3954
|
+
"}",
|
|
3955
|
+
`body[data-ds-dark-theme] .${CLAUDE_MARKDOWN_SCOPE}{`,
|
|
3956
|
+
palette(PIERRE_DARK, "#1a1a19", "#1a1a19", INLINE_FILL_DARK),
|
|
3957
|
+
"}",
|
|
3958
|
+
`.${CLAUDE_MARKDOWN_SCOPE} :not(pre)>code{color:${CLAY_EMPHASIZED};border-radius:4px;padding:1px 2px}`,
|
|
3959
|
+
`body[data-ds-dark-theme] .${CLAUDE_MARKDOWN_SCOPE} :not(pre)>code{color:${CLAY}}`,
|
|
3960
|
+
`.${CLAUDE_MARKDOWN_SCOPE} [class*="bannerWrap"]{position:absolute;top:0;right:0;z-index:7;background:transparent;border-radius:0}`,
|
|
3961
|
+
`.${CLAUDE_MARKDOWN_SCOPE} [class*="banner"]:not([class*="bannerWrap"]){background:transparent;padding:6px 8px}`,
|
|
3962
|
+
`.${CLAUDE_MARKDOWN_SCOPE} [class*="infostring"]{display:none}`,
|
|
3963
|
+
`.${CLAUDE_MARKDOWN_SCOPE} pre{white-space:pre;word-break:normal;overflow-x:auto}`,
|
|
3964
|
+
`.${CLAUDE_MARKDOWN_SCOPE} [class*="block"]{--dsl-code-block-border-radius:${BLOCK_RADIUS}}`
|
|
3965
|
+
].join("");
|
|
3966
|
+
let injected = false;
|
|
3967
|
+
/** Attach the sheet once per page. */
|
|
3968
|
+
function ensureClaudeMarkdownTheme() {
|
|
3969
|
+
if (injected || typeof document === "undefined") return;
|
|
3970
|
+
injected = true;
|
|
3971
|
+
const element = document.createElement("style");
|
|
3972
|
+
element.dataset.dshClaudeMarkdownTheme = "";
|
|
3973
|
+
element.textContent = CLAUDE_MARKDOWN_THEME_CSS;
|
|
3974
|
+
document.head.appendChild(element);
|
|
3975
|
+
}
|
|
3976
|
+
//#endregion
|
|
3739
3977
|
//#region src/client/markdown-labels.tsx
|
|
3740
3978
|
/** Stable across renders: MarkdownText keys its streaming renderer on this
|
|
3741
3979
|
* object's identity and reparses from scratch whenever it changes. */
|
|
@@ -3755,18 +3993,298 @@ window.__ModuleLoader__.load({
|
|
|
3755
3993
|
* than repeated at every call site. Drop it once the installed
|
|
3756
3994
|
* @deepseek-ai/dsh-client-ui-primitives matches the Desktop build. */
|
|
3757
3995
|
function ClaudeMarkdown({ text, labels, streaming }) {
|
|
3758
|
-
|
|
3996
|
+
const props = {
|
|
3759
3997
|
text,
|
|
3760
3998
|
labels,
|
|
3761
3999
|
...streaming === void 0 ? {} : { streaming }
|
|
4000
|
+
};
|
|
4001
|
+
const Renderer = MarkdownText;
|
|
4002
|
+
ensureClaudeMarkdownTheme();
|
|
4003
|
+
return /* @__PURE__ */ jsx("div", {
|
|
4004
|
+
className: CLAUDE_MARKDOWN_SCOPE,
|
|
4005
|
+
children: /* @__PURE__ */ jsx(Renderer, { ...props })
|
|
3762
4006
|
});
|
|
3763
4007
|
}
|
|
3764
4008
|
//#endregion
|
|
4009
|
+
//#region src/plugin-budget.ts
|
|
4010
|
+
/**
|
|
4011
|
+
* The plugin's connection budget, in one table.
|
|
4012
|
+
*
|
|
4013
|
+
* A browser opens a small fixed number of connections to one origin — six for
|
|
4014
|
+
* HTTP/1.1 in Chromium — and shares them with the Host's own traffic. Every
|
|
4015
|
+
* response this plugin holds open costs one of them for its lifetime, and a
|
|
4016
|
+
* request that cannot get one waits in the browser's queue where no server-side
|
|
4017
|
+
* deadline can reach it. When the pool is exhausted the panels that would
|
|
4018
|
+
* *diagnose* the problem are the first thing to stop answering, which is how
|
|
4019
|
+
* this failure has always presented: four settings cards timing out at once
|
|
4020
|
+
* against a Host that is demonstrably healthy.
|
|
4021
|
+
*
|
|
4022
|
+
* So the budget is a fixed constant rather than a function of how much work is
|
|
4023
|
+
* in flight. Steady state is one connection (the multiplexed projection
|
|
4024
|
+
* carrier); the peak is `PLUGIN_GLOBAL_PERMITS`, whatever the session count.
|
|
4025
|
+
*
|
|
4026
|
+
* Both halves read this file, which is the point: a route declares a budget
|
|
4027
|
+
* class and the client derives its wait from the same entry, so a server
|
|
4028
|
+
* deadline can never be quietly longer than the client's patience.
|
|
4029
|
+
*/
|
|
4030
|
+
/** Server-side budget classes. A route declares a class, never a number. */
|
|
4031
|
+
const ROUTE_BUDGET_MS = {
|
|
4032
|
+
/** Answers from memory or a single bounded probe. */
|
|
4033
|
+
fast: 5e3,
|
|
4034
|
+
/** Chains local Git work. */
|
|
4035
|
+
git: 45e3,
|
|
4036
|
+
/** Reaches the network: remote Git, `gh`, the npm registry. */
|
|
4037
|
+
remote: 15e4
|
|
4038
|
+
};
|
|
4039
|
+
/** The client waits one round trip longer, so the route's own 504 wins the
|
|
4040
|
+
* race and the caller learns which budget elapsed instead of guessing. */
|
|
4041
|
+
const CLIENT_GRACE_MS = 3e3;
|
|
4042
|
+
function clientBudgetMs(budget) {
|
|
4043
|
+
return ROUTE_BUDGET_MS[budget] + CLIENT_GRACE_MS;
|
|
4044
|
+
}
|
|
4045
|
+
/** A request that never got a permit fails fast and says so, rather than
|
|
4046
|
+
* spending its whole budget queued behind work it cannot see. */
|
|
4047
|
+
const QUEUE_WAIT_BUDGET_MS = 4e3;
|
|
4048
|
+
/** Per-lane ceilings inside the global budget.
|
|
4049
|
+
*
|
|
4050
|
+
* The projection carrier holds a permanently reserved permit outside these,
|
|
4051
|
+
* so three remain. `write + stream <= 2` leaves one permit that only a read
|
|
4052
|
+
* can take: a diagnostic read always has somewhere to go, however many slow
|
|
4053
|
+
* actions are in flight. That invariant is what stops a 150s repository
|
|
4054
|
+
* action from reproducing the original symptom through a new mechanism. */
|
|
4055
|
+
const PLUGIN_LANE_CAPS = {
|
|
4056
|
+
read: 2,
|
|
4057
|
+
write: 1,
|
|
4058
|
+
stream: 1
|
|
4059
|
+
};
|
|
4060
|
+
//#endregion
|
|
3765
4061
|
//#region src/rewind.ts
|
|
3766
4062
|
function isRewound(ranges, seq) {
|
|
3767
4063
|
return ranges.some((range) => seq >= range.start && seq <= range.end);
|
|
3768
4064
|
}
|
|
3769
4065
|
//#endregion
|
|
4066
|
+
//#region src/client/plugin-transport.ts
|
|
4067
|
+
/**
|
|
4068
|
+
* The plugin's only door to the network.
|
|
4069
|
+
*
|
|
4070
|
+
* Every request the Client makes goes through here, and the shape of the
|
|
4071
|
+
* public functions is the point. There is no `signal` property, no `headers`,
|
|
4072
|
+
* no `RequestInit` and no millisecond number in any parameter: a caller cannot
|
|
4073
|
+
* hand this module an options object whose later spread silently overwrites
|
|
4074
|
+
* the deadline sitting above it. That is not a hypothetical — it is exactly
|
|
4075
|
+
* how four call sites lost their deadlines while looking completely ordinary,
|
|
4076
|
+
* because `exactOptionalPropertyTypes` forbids writing `signal: undefined` and
|
|
4077
|
+
* the spread idiom is what people reach for instead. `cancel` is positional
|
|
4078
|
+
* here, so `undefined` is simply passable and the idiom has nothing to do.
|
|
4079
|
+
*
|
|
4080
|
+
* The second seal is arithmetic. The browser shares a small fixed connection
|
|
4081
|
+
* budget between this plugin and the Host, and the plugin used to spend it
|
|
4082
|
+
* proportionally to how many Claude sessions existed. Here every request takes
|
|
4083
|
+
* a permit from a fixed pool first, so more call sites and more sessions
|
|
4084
|
+
* cannot become more sockets — a saturated plugin queues, and says `starved`
|
|
4085
|
+
* within `QUEUE_WAIT_BUDGET_MS` instead of dying silently at its full budget.
|
|
4086
|
+
*
|
|
4087
|
+
* Lane caps guarantee a read always has somewhere to go: the projection
|
|
4088
|
+
* carrier holds a reserved permit, and `write + stream` can occupy at most two
|
|
4089
|
+
* of the remaining three. The panel that diagnoses a saturated pool is
|
|
4090
|
+
* therefore the one request that cannot be starved by it.
|
|
4091
|
+
*/
|
|
4092
|
+
var PluginRequestError = class extends Error {
|
|
4093
|
+
reason;
|
|
4094
|
+
status;
|
|
4095
|
+
code;
|
|
4096
|
+
constructor(reason, message, status, code) {
|
|
4097
|
+
super(message);
|
|
4098
|
+
this.name = "PluginRequestError";
|
|
4099
|
+
this.reason = reason;
|
|
4100
|
+
if (status !== void 0) this.status = status;
|
|
4101
|
+
if (code !== void 0) this.code = code;
|
|
4102
|
+
}
|
|
4103
|
+
};
|
|
4104
|
+
let send = (...args) => fetch(...args);
|
|
4105
|
+
let held = 0;
|
|
4106
|
+
const laneHeld = {
|
|
4107
|
+
read: 0,
|
|
4108
|
+
write: 0,
|
|
4109
|
+
stream: 0
|
|
4110
|
+
};
|
|
4111
|
+
let projectionHeld = false;
|
|
4112
|
+
let queue = [];
|
|
4113
|
+
const inFlight = /* @__PURE__ */ new Map();
|
|
4114
|
+
/** The projection carrier owns a permit of its own, outside the lane caps, so
|
|
4115
|
+
* the transcript stream and the panels never compete for the same slot. */
|
|
4116
|
+
function laneHasRoom(lane) {
|
|
4117
|
+
return held < 4 && laneHeld[lane] < PLUGIN_LANE_CAPS[lane];
|
|
4118
|
+
}
|
|
4119
|
+
function pump() {
|
|
4120
|
+
for (let index = 0; index < queue.length; index += 1) {
|
|
4121
|
+
const waiter = queue[index];
|
|
4122
|
+
if (waiter === void 0 || !laneHasRoom(waiter.lane)) continue;
|
|
4123
|
+
queue.splice(index, 1);
|
|
4124
|
+
index -= 1;
|
|
4125
|
+
clearTimeout(waiter.timer);
|
|
4126
|
+
held += 1;
|
|
4127
|
+
laneHeld[waiter.lane] += 1;
|
|
4128
|
+
waiter.admit();
|
|
4129
|
+
}
|
|
4130
|
+
}
|
|
4131
|
+
function release(lane) {
|
|
4132
|
+
held -= 1;
|
|
4133
|
+
laneHeld[lane] -= 1;
|
|
4134
|
+
pump();
|
|
4135
|
+
}
|
|
4136
|
+
function acquire(lane) {
|
|
4137
|
+
let released = false;
|
|
4138
|
+
const releaseOnce = () => {
|
|
4139
|
+
if (released) return;
|
|
4140
|
+
released = true;
|
|
4141
|
+
release(lane);
|
|
4142
|
+
};
|
|
4143
|
+
if (laneHasRoom(lane)) {
|
|
4144
|
+
held += 1;
|
|
4145
|
+
laneHeld[lane] += 1;
|
|
4146
|
+
return Promise.resolve(releaseOnce);
|
|
4147
|
+
}
|
|
4148
|
+
return new Promise((resolve, reject) => {
|
|
4149
|
+
const waiter = {
|
|
4150
|
+
lane,
|
|
4151
|
+
admit: () => resolve(releaseOnce),
|
|
4152
|
+
reject,
|
|
4153
|
+
timer: setTimeout(() => {
|
|
4154
|
+
queue = queue.filter((item) => item !== waiter);
|
|
4155
|
+
reject(new PluginRequestError("starved", "The plugin is holding every connection it is allowed to open."));
|
|
4156
|
+
}, QUEUE_WAIT_BUDGET_MS)
|
|
4157
|
+
};
|
|
4158
|
+
queue.push(waiter);
|
|
4159
|
+
});
|
|
4160
|
+
}
|
|
4161
|
+
function withQuery(path, query) {
|
|
4162
|
+
if (query === void 0) return path;
|
|
4163
|
+
const encoded = new URLSearchParams(query).toString();
|
|
4164
|
+
return encoded.length === 0 ? path : `${path}?${encoded}`;
|
|
4165
|
+
}
|
|
4166
|
+
function failureOf(error, cancel) {
|
|
4167
|
+
if (error instanceof PluginRequestError) return error;
|
|
4168
|
+
if (cancel?.aborted === true) return new PluginRequestError("cancelled", "The caller cancelled the request.");
|
|
4169
|
+
const name = error instanceof Error ? error.name : "";
|
|
4170
|
+
if (name === "TimeoutError" || name === "AbortError") return new PluginRequestError("timeout", "The plugin route did not answer inside its budget.");
|
|
4171
|
+
return new PluginRequestError("http", error instanceof Error ? error.message : String(error));
|
|
4172
|
+
}
|
|
4173
|
+
async function decode(response) {
|
|
4174
|
+
let payload;
|
|
4175
|
+
try {
|
|
4176
|
+
payload = await response.json();
|
|
4177
|
+
} catch {
|
|
4178
|
+
if (response.ok) throw new PluginRequestError("shape", "The plugin route answered with a body this build cannot read.");
|
|
4179
|
+
throw new PluginRequestError("http", `HTTP ${response.status}`, response.status);
|
|
4180
|
+
}
|
|
4181
|
+
if (response.ok) return payload;
|
|
4182
|
+
if (response.status === 404) throw new PluginRequestError("route-missing", "The Host is running an older plugin build without this route.", 404);
|
|
4183
|
+
const record = typeof payload === "object" && payload !== null ? payload : void 0;
|
|
4184
|
+
const message = typeof record?.message === "string" ? record.message : typeof record?.error === "string" ? record.error : `HTTP ${response.status}`;
|
|
4185
|
+
const code = typeof record?.error === "string" ? record.error : void 0;
|
|
4186
|
+
throw new PluginRequestError("http", message, response.status, code);
|
|
4187
|
+
}
|
|
4188
|
+
async function dispatch(lane, method, path, budget, cancel, options) {
|
|
4189
|
+
const url = withQuery(path, options?.query);
|
|
4190
|
+
const key = options?.key ?? `${method} ${url}`;
|
|
4191
|
+
const existing = inFlight.get(key);
|
|
4192
|
+
if (existing !== void 0) return await existing;
|
|
4193
|
+
const run = (async () => {
|
|
4194
|
+
const free = await acquire(lane);
|
|
4195
|
+
try {
|
|
4196
|
+
const timeout = AbortSignal.timeout(clientBudgetMs(budget));
|
|
4197
|
+
const signal = cancel === void 0 ? timeout : AbortSignal.any([cancel, timeout]);
|
|
4198
|
+
return await decode(await send(url, {
|
|
4199
|
+
method,
|
|
4200
|
+
credentials: "same-origin",
|
|
4201
|
+
signal,
|
|
4202
|
+
headers: options?.json === void 0 ? { accept: "application/json" } : {
|
|
4203
|
+
accept: "application/json",
|
|
4204
|
+
"content-type": "application/json"
|
|
4205
|
+
},
|
|
4206
|
+
...options?.json === void 0 ? {} : { body: JSON.stringify(options.json) }
|
|
4207
|
+
}));
|
|
4208
|
+
} catch (error) {
|
|
4209
|
+
throw failureOf(error, cancel);
|
|
4210
|
+
} finally {
|
|
4211
|
+
free();
|
|
4212
|
+
}
|
|
4213
|
+
})();
|
|
4214
|
+
inFlight.set(key, run);
|
|
4215
|
+
try {
|
|
4216
|
+
return await run;
|
|
4217
|
+
} finally {
|
|
4218
|
+
if (inFlight.get(key) === run) inFlight.delete(key);
|
|
4219
|
+
}
|
|
4220
|
+
}
|
|
4221
|
+
/** A bounded read of a plugin route. */
|
|
4222
|
+
function pluginRead(path, budget, cancel, options) {
|
|
4223
|
+
return dispatch("read", "GET", path, budget, cancel, options);
|
|
4224
|
+
}
|
|
4225
|
+
/** A bounded write. Writes never coalesce by default: two of them are two
|
|
4226
|
+
* intents, even when their bodies match. */
|
|
4227
|
+
function pluginWrite(path, budget, cancel, options) {
|
|
4228
|
+
const method = options?.method ?? "POST";
|
|
4229
|
+
return dispatch("write", method, path, budget, cancel, {
|
|
4230
|
+
...options,
|
|
4231
|
+
key: options?.key ?? `${method} ${withQuery(path, options?.query)} #${nextWriteId()}`
|
|
4232
|
+
});
|
|
4233
|
+
}
|
|
4234
|
+
let writeId = 0;
|
|
4235
|
+
function nextWriteId() {
|
|
4236
|
+
writeId += 1;
|
|
4237
|
+
return writeId;
|
|
4238
|
+
}
|
|
4239
|
+
async function openStream(lane, path, cancel, options, reserved) {
|
|
4240
|
+
const url = withQuery(path, options?.query);
|
|
4241
|
+
const free = reserved ? () => {
|
|
4242
|
+
projectionHeld = false;
|
|
4243
|
+
} : await acquire(lane);
|
|
4244
|
+
try {
|
|
4245
|
+
const response = await send(url, {
|
|
4246
|
+
method: options?.method ?? "GET",
|
|
4247
|
+
credentials: "same-origin",
|
|
4248
|
+
signal: cancel,
|
|
4249
|
+
headers: options?.json === void 0 ? { accept: "application/x-ndjson" } : {
|
|
4250
|
+
accept: "application/x-ndjson",
|
|
4251
|
+
"content-type": "application/json"
|
|
4252
|
+
},
|
|
4253
|
+
...options?.json === void 0 ? {} : { body: JSON.stringify(options.json) }
|
|
4254
|
+
});
|
|
4255
|
+
if (!response.ok || response.body === null) {
|
|
4256
|
+
free();
|
|
4257
|
+
if (response.status === 404) throw new PluginRequestError("route-missing", "The Host is running an older plugin build without this route.", 404);
|
|
4258
|
+
throw new PluginRequestError("http", `HTTP ${response.status}`, response.status);
|
|
4259
|
+
}
|
|
4260
|
+
cancel.addEventListener("abort", free, { once: true });
|
|
4261
|
+
return response.body.getReader();
|
|
4262
|
+
} catch (error) {
|
|
4263
|
+
free();
|
|
4264
|
+
throw failureOf(error, cancel);
|
|
4265
|
+
}
|
|
4266
|
+
}
|
|
4267
|
+
/** A long-lived NDJSON response. No deadline — it is meant to stay open — but
|
|
4268
|
+
* it takes a counted permit for its whole life, which is the bound that
|
|
4269
|
+
* matters. */
|
|
4270
|
+
function pluginNdjson(path, cancel, options) {
|
|
4271
|
+
return openStream("stream", path, cancel, options, false);
|
|
4272
|
+
}
|
|
4273
|
+
/** The reserved projection carrier: exactly one live connection, ever,
|
|
4274
|
+
* whatever the session count. */
|
|
4275
|
+
function pluginProjectionStream(path, cancel, options) {
|
|
4276
|
+
if (projectionHeld) return Promise.reject(new PluginRequestError("starved", "The projection carrier is already open."));
|
|
4277
|
+
projectionHeld = true;
|
|
4278
|
+
return openStream("stream", path, cancel, options, true);
|
|
4279
|
+
}
|
|
4280
|
+
/** Fire-and-forget diagnostics. Dropped rather than queued when saturated:
|
|
4281
|
+
* the channel that reports the plugin's own failures must never be the
|
|
4282
|
+
* traffic that causes them. */
|
|
4283
|
+
function pluginBeacon(path, body) {
|
|
4284
|
+
if (!laneHasRoom("write")) return;
|
|
4285
|
+
pluginWrite(path, "fast", void 0, { json: body }).catch(() => void 0);
|
|
4286
|
+
}
|
|
4287
|
+
//#endregion
|
|
3770
4288
|
//#region src/client/projection.ts
|
|
3771
4289
|
const EMPTY_CLAUDE_PROJECTION = {
|
|
3772
4290
|
schemaVersion: 1,
|
|
@@ -3776,6 +4294,13 @@ window.__ModuleLoader__.load({
|
|
|
3776
4294
|
activities: []
|
|
3777
4295
|
};
|
|
3778
4296
|
const RETRY_DELAY_MS = 2e3;
|
|
4297
|
+
/** Floor between carrier reopens forced by a desync. A carrier that is losing
|
|
4298
|
+
* lines must not be answered with a reconnect per lost line. */
|
|
4299
|
+
const RESYNC_COOLDOWN_MS = 5e3;
|
|
4300
|
+
/** Wait for the subscribed set to stop moving before reopening the carrier:
|
|
4301
|
+
* mounting a session list changes it once per row. */
|
|
4302
|
+
const SUBSCRIPTION_SETTLE_MS = 250;
|
|
4303
|
+
const NDJSON_SEPARATOR = String.fromCharCode(10);
|
|
3779
4304
|
/** Coalesce stream deltas into at most one React notification per frame. */
|
|
3780
4305
|
const FRAME_MS = 16;
|
|
3781
4306
|
/** Typewriter smoothing: drain newly arrived prose over roughly this window,
|
|
@@ -3899,11 +4424,19 @@ window.__ModuleLoader__.load({
|
|
|
3899
4424
|
setTimeout(resolve, ms).unref?.();
|
|
3900
4425
|
});
|
|
3901
4426
|
}
|
|
3902
|
-
/**
|
|
3903
|
-
*
|
|
3904
|
-
|
|
4427
|
+
/** One session's reducer over the shared carrier's lines.
|
|
4428
|
+
*
|
|
4429
|
+
* A source opens nothing. It used to hold a stream of its own, which made the
|
|
4430
|
+
* plugin's connection count a function of how many sessions existed;
|
|
4431
|
+
* {@link ClaudeProjectionStore} now owns the single carrier and feeds each
|
|
4432
|
+
* session's lines here. `onDemand` reports whether anyone is watching, which
|
|
4433
|
+
* is what the store uses to decide which sessions the carrier subscribes to. */
|
|
4434
|
+
function createClaudeProjectionSource(sessionId, onDemand = () => {}, onDesync = () => {}) {
|
|
3905
4435
|
let snapshot = EMPTY_CLAUDE_PROJECTION;
|
|
3906
4436
|
let revision = 0;
|
|
4437
|
+
/** Last carrier line this reducer applied, by the server's count. Undefined
|
|
4438
|
+
* until a snapshot states where the stream stands. */
|
|
4439
|
+
let seq;
|
|
3907
4440
|
let owned = false;
|
|
3908
4441
|
let commands = [];
|
|
3909
4442
|
let contextUsage;
|
|
@@ -3916,8 +4449,6 @@ window.__ModuleLoader__.load({
|
|
|
3916
4449
|
/** Streaming prose still being revealed: full arrived text plus shown chars. */
|
|
3917
4450
|
const reveal = /* @__PURE__ */ new Map();
|
|
3918
4451
|
let disposed = false;
|
|
3919
|
-
let running = false;
|
|
3920
|
-
let controller;
|
|
3921
4452
|
let frame;
|
|
3922
4453
|
let usedAnimationFrame = false;
|
|
3923
4454
|
const listeners = /* @__PURE__ */ new Set();
|
|
@@ -4016,7 +4547,7 @@ window.__ModuleLoader__.load({
|
|
|
4016
4547
|
}
|
|
4017
4548
|
};
|
|
4018
4549
|
const applyText = (event) => {
|
|
4019
|
-
const { turn, step, ordinal, append, text } = event;
|
|
4550
|
+
const { turn, step, ordinal, append, text, renderer } = event;
|
|
4020
4551
|
if (!nonNegativeInteger(turn) || !nonNegativeInteger(step) || !nonNegativeInteger(ordinal)) return false;
|
|
4021
4552
|
if (append !== void 0 && (typeof append !== "string" || append.length > MAX_TRANSCRIPT_CHARS)) return false;
|
|
4022
4553
|
if (text !== void 0 && (typeof text !== "string" || text.length > MAX_TRANSCRIPT_CHARS)) return false;
|
|
@@ -4033,7 +4564,8 @@ window.__ModuleLoader__.load({
|
|
|
4033
4564
|
step,
|
|
4034
4565
|
ordinal,
|
|
4035
4566
|
kind: "text",
|
|
4036
|
-
phase: "updated"
|
|
4567
|
+
phase: "updated",
|
|
4568
|
+
...isClaudeRenderMode(renderer) ? { renderer } : {}
|
|
4037
4569
|
},
|
|
4038
4570
|
text: fullText
|
|
4039
4571
|
};
|
|
@@ -4053,6 +4585,17 @@ window.__ModuleLoader__.load({
|
|
|
4053
4585
|
});
|
|
4054
4586
|
return true;
|
|
4055
4587
|
};
|
|
4588
|
+
/** This reducer is behind the server and cannot catch up on its own: only a
|
|
4589
|
+
* fresh snapshot can. Reported as well as acted on, because a silent
|
|
4590
|
+
* self-heal hides how often the carrier is losing lines.
|
|
4591
|
+
*
|
|
4592
|
+
* Numbering stops until that snapshot arrives and states it again; every
|
|
4593
|
+
* line in between would only be measured against a count already known to
|
|
4594
|
+
* be wrong. */
|
|
4595
|
+
const desync = (kind, detail) => {
|
|
4596
|
+
seq = void 0;
|
|
4597
|
+
onDesync(kind, `${sessionId}: ${detail}`);
|
|
4598
|
+
};
|
|
4056
4599
|
const applyLine = (line) => {
|
|
4057
4600
|
const trimmed = line.trim();
|
|
4058
4601
|
if (trimmed.length === 0) return;
|
|
@@ -4064,10 +4607,16 @@ window.__ModuleLoader__.load({
|
|
|
4064
4607
|
}
|
|
4065
4608
|
const event = record$7(value);
|
|
4066
4609
|
if (event === void 0 || typeof event.type !== "string") return;
|
|
4610
|
+
if (event.type === "checkpoint") {
|
|
4611
|
+
if (nonNegativeInteger(event.seq) && seq !== void 0 && event.seq !== seq) desync("projection-gap", `checkpoint at ${event.seq}, applied ${seq}`);
|
|
4612
|
+
return;
|
|
4613
|
+
}
|
|
4614
|
+
if (event.type !== "snapshot" && nonNegativeInteger(event.seq) && seq !== void 0 && event.seq !== seq + 1) desync("projection-gap", `expected ${seq + 1}, received ${event.seq}`);
|
|
4067
4615
|
try {
|
|
4068
4616
|
switch (event.type) {
|
|
4069
4617
|
case "snapshot": {
|
|
4070
4618
|
const next = parseClaudeClientProjection(event);
|
|
4619
|
+
seq = nonNegativeInteger(event.seq) ? event.seq : void 0;
|
|
4071
4620
|
revision = next.revision;
|
|
4072
4621
|
owned = next.owned;
|
|
4073
4622
|
commands = next.commands;
|
|
@@ -4080,7 +4629,10 @@ window.__ModuleLoader__.load({
|
|
|
4080
4629
|
break;
|
|
4081
4630
|
}
|
|
4082
4631
|
case "text":
|
|
4083
|
-
if (!applyText(event))
|
|
4632
|
+
if (!applyText(event)) {
|
|
4633
|
+
if (seq !== void 0) desync("projection-delta-rejected", "text not applied");
|
|
4634
|
+
return;
|
|
4635
|
+
}
|
|
4084
4636
|
revision += 1;
|
|
4085
4637
|
break;
|
|
4086
4638
|
case "activity":
|
|
@@ -4114,87 +4666,185 @@ window.__ModuleLoader__.load({
|
|
|
4114
4666
|
default: return;
|
|
4115
4667
|
}
|
|
4116
4668
|
} catch {
|
|
4669
|
+
desync("projection-delta-rejected", `rejected ${event.type}`);
|
|
4117
4670
|
return;
|
|
4118
4671
|
}
|
|
4672
|
+
if (nonNegativeInteger(event.seq)) seq = event.seq;
|
|
4119
4673
|
schedulePublish();
|
|
4120
4674
|
};
|
|
4121
|
-
const run = async () => {
|
|
4122
|
-
if (running) return;
|
|
4123
|
-
running = true;
|
|
4124
|
-
try {
|
|
4125
|
-
while (!disposed && listeners.size > 0) {
|
|
4126
|
-
controller = new AbortController();
|
|
4127
|
-
try {
|
|
4128
|
-
const response = await fetchProjection(`${CLAUDE_PROJECTION_PATH}/${encodeURIComponent(sessionId)}/stream`, {
|
|
4129
|
-
headers: { accept: "application/x-ndjson" },
|
|
4130
|
-
signal: controller.signal
|
|
4131
|
-
});
|
|
4132
|
-
if (!response.ok) throw new Error(`Claude projection stream failed (${response.status})`);
|
|
4133
|
-
if (response.body === null) throw new Error("Claude projection stream is unavailable");
|
|
4134
|
-
const reader = response.body.getReader();
|
|
4135
|
-
const decoder = new TextDecoder();
|
|
4136
|
-
let buffer = "";
|
|
4137
|
-
while (true) {
|
|
4138
|
-
if (disposed || listeners.size === 0) {
|
|
4139
|
-
await reader.cancel().catch(() => void 0);
|
|
4140
|
-
break;
|
|
4141
|
-
}
|
|
4142
|
-
const chunk = await reader.read();
|
|
4143
|
-
buffer += decoder.decode(chunk.value, { stream: !chunk.done });
|
|
4144
|
-
const lines = buffer.split("\n");
|
|
4145
|
-
buffer = lines.pop() ?? "";
|
|
4146
|
-
for (const line of lines) applyLine(line);
|
|
4147
|
-
if (chunk.done) break;
|
|
4148
|
-
}
|
|
4149
|
-
} catch (error) {
|
|
4150
|
-
if (isAbort(error)) return;
|
|
4151
|
-
} finally {
|
|
4152
|
-
controller = void 0;
|
|
4153
|
-
}
|
|
4154
|
-
if (disposed || listeners.size === 0) return;
|
|
4155
|
-
await delay(retryDelayMs);
|
|
4156
|
-
}
|
|
4157
|
-
} finally {
|
|
4158
|
-
running = false;
|
|
4159
|
-
}
|
|
4160
|
-
};
|
|
4161
4675
|
return {
|
|
4162
4676
|
getSnapshot: () => snapshot,
|
|
4677
|
+
feed: applyLine,
|
|
4163
4678
|
subscribe(listener) {
|
|
4164
4679
|
if (disposed) return () => {};
|
|
4165
4680
|
const wasIdle = listeners.size === 0;
|
|
4166
4681
|
listeners.add(listener);
|
|
4167
|
-
if (wasIdle)
|
|
4682
|
+
if (wasIdle) onDemand(true);
|
|
4168
4683
|
return () => {
|
|
4169
4684
|
listeners.delete(listener);
|
|
4170
4685
|
if (listeners.size !== 0) return;
|
|
4171
|
-
controller?.abort();
|
|
4172
|
-
controller = void 0;
|
|
4173
4686
|
cancelFrame();
|
|
4687
|
+
onDemand(false);
|
|
4174
4688
|
};
|
|
4175
4689
|
},
|
|
4176
4690
|
dispose() {
|
|
4177
4691
|
disposed = true;
|
|
4178
4692
|
listeners.clear();
|
|
4179
|
-
controller?.abort();
|
|
4180
|
-
controller = void 0;
|
|
4181
4693
|
cancelFrame();
|
|
4694
|
+
onDemand(false);
|
|
4182
4695
|
}
|
|
4183
4696
|
};
|
|
4184
4697
|
}
|
|
4698
|
+
/**
|
|
4699
|
+
* Every session's projection over ONE connection.
|
|
4700
|
+
*
|
|
4701
|
+
* The plugin used to open an NDJSON stream per session, so its share of the
|
|
4702
|
+
* browser's small per-origin connection budget grew with the number of Claude
|
|
4703
|
+
* sessions — and the overview panel subscribes one per LISTED session, not per
|
|
4704
|
+
* open one. Past a handful of sessions the plugin's own settings panel could no
|
|
4705
|
+
* longer get a connection at all, which is the failure this class exists to
|
|
4706
|
+
* make impossible: the carrier is one connection whatever the session count.
|
|
4707
|
+
*
|
|
4708
|
+
* `source(sessionId)` keeps its shape, so consumers are unaware of any of this.
|
|
4709
|
+
*/
|
|
4185
4710
|
var ClaudeProjectionStore = class {
|
|
4186
4711
|
#sources = /* @__PURE__ */ new Map();
|
|
4712
|
+
/** Sessions with at least one live subscriber, newest interest last. */
|
|
4713
|
+
#wanted = /* @__PURE__ */ new Set();
|
|
4714
|
+
#open;
|
|
4715
|
+
#retryDelayMs;
|
|
4716
|
+
#settleMs;
|
|
4717
|
+
#report;
|
|
4718
|
+
#resyncCooldownMs;
|
|
4719
|
+
#resyncedAt = 0;
|
|
4720
|
+
#controller;
|
|
4721
|
+
#settle;
|
|
4722
|
+
#running = false;
|
|
4723
|
+
#disposed = false;
|
|
4724
|
+
constructor(options = {}) {
|
|
4725
|
+
this.#open = options.open ?? ((path, cancel) => pluginProjectionStream(path, cancel));
|
|
4726
|
+
this.#retryDelayMs = options.retryDelayMs ?? RETRY_DELAY_MS;
|
|
4727
|
+
this.#settleMs = options.settleMs ?? SUBSCRIPTION_SETTLE_MS;
|
|
4728
|
+
this.#report = options.report ?? (() => {});
|
|
4729
|
+
this.#resyncCooldownMs = options.resyncCooldownMs ?? RESYNC_COOLDOWN_MS;
|
|
4730
|
+
}
|
|
4187
4731
|
source(sessionId) {
|
|
4188
4732
|
let source = this.#sources.get(sessionId);
|
|
4189
4733
|
if (source === void 0) {
|
|
4190
|
-
source = createClaudeProjectionSource(sessionId)
|
|
4734
|
+
source = createClaudeProjectionSource(sessionId, (active) => {
|
|
4735
|
+
this.#demand(sessionId, active);
|
|
4736
|
+
}, (kind, detail) => {
|
|
4737
|
+
this.#resync(kind, detail);
|
|
4738
|
+
});
|
|
4191
4739
|
this.#sources.set(sessionId, source);
|
|
4192
4740
|
}
|
|
4193
4741
|
return source;
|
|
4194
4742
|
}
|
|
4743
|
+
/** Reopen the carrier so every lane is restated from a fresh snapshot.
|
|
4744
|
+
*
|
|
4745
|
+
* One session noticed the hole, but the carrier is shared and a dropped
|
|
4746
|
+
* line is a property of the carrier, so the others are suspect too --
|
|
4747
|
+
* reopening restates all of them for the price of the one reconnect. */
|
|
4748
|
+
#resync(kind, detail) {
|
|
4749
|
+
if (this.#disposed) return;
|
|
4750
|
+
this.#report(kind, detail);
|
|
4751
|
+
const now = Date.now();
|
|
4752
|
+
if (now - this.#resyncedAt < this.#resyncCooldownMs) return;
|
|
4753
|
+
this.#resyncedAt = now;
|
|
4754
|
+
this.#reopen();
|
|
4755
|
+
}
|
|
4195
4756
|
dispose() {
|
|
4757
|
+
this.#disposed = true;
|
|
4758
|
+
if (this.#settle !== void 0) clearTimeout(this.#settle);
|
|
4759
|
+
this.#settle = void 0;
|
|
4760
|
+
this.#controller?.abort();
|
|
4761
|
+
this.#controller = void 0;
|
|
4196
4762
|
for (const source of this.#sources.values()) source.dispose();
|
|
4197
4763
|
this.#sources.clear();
|
|
4764
|
+
this.#wanted.clear();
|
|
4765
|
+
}
|
|
4766
|
+
/** Note interest and reopen the carrier once the set stops moving. Mounting
|
|
4767
|
+
* a session list would otherwise reopen it once per row. */
|
|
4768
|
+
#demand(sessionId, active) {
|
|
4769
|
+
if (this.#disposed) return;
|
|
4770
|
+
if (active) {
|
|
4771
|
+
this.#wanted.delete(sessionId);
|
|
4772
|
+
this.#wanted.add(sessionId);
|
|
4773
|
+
} else if (!this.#wanted.delete(sessionId)) return;
|
|
4774
|
+
if (this.#settle !== void 0) clearTimeout(this.#settle);
|
|
4775
|
+
const timer = setTimeout(() => {
|
|
4776
|
+
this.#settle = void 0;
|
|
4777
|
+
this.#reopen();
|
|
4778
|
+
}, this.#settleMs);
|
|
4779
|
+
timer.unref?.();
|
|
4780
|
+
this.#settle = timer;
|
|
4781
|
+
}
|
|
4782
|
+
#lanes() {
|
|
4783
|
+
const wanted = [...this.#wanted];
|
|
4784
|
+
return wanted.slice(Math.max(0, wanted.length - 16));
|
|
4785
|
+
}
|
|
4786
|
+
#reopen() {
|
|
4787
|
+
this.#controller?.abort();
|
|
4788
|
+
this.#controller = void 0;
|
|
4789
|
+
if (this.#disposed || this.#wanted.size === 0) return;
|
|
4790
|
+
this.#run();
|
|
4791
|
+
}
|
|
4792
|
+
async #run() {
|
|
4793
|
+
if (this.#running) return;
|
|
4794
|
+
this.#running = true;
|
|
4795
|
+
try {
|
|
4796
|
+
while (!this.#disposed && this.#wanted.size > 0) {
|
|
4797
|
+
const controller = new AbortController();
|
|
4798
|
+
this.#controller = controller;
|
|
4799
|
+
const lanes = this.#lanes();
|
|
4800
|
+
let superseded = false;
|
|
4801
|
+
try {
|
|
4802
|
+
const reader = await this.#open(`${CLAUDE_PROJECTION_PATH}/multi?sessions=${lanes.map(encodeURIComponent).join(",")}`, controller.signal);
|
|
4803
|
+
const stop = () => {
|
|
4804
|
+
superseded = true;
|
|
4805
|
+
reader.cancel().catch(() => void 0);
|
|
4806
|
+
};
|
|
4807
|
+
controller.signal.addEventListener("abort", stop, { once: true });
|
|
4808
|
+
const decoder = new TextDecoder();
|
|
4809
|
+
let buffer = "";
|
|
4810
|
+
while (!controller.signal.aborted) {
|
|
4811
|
+
const chunk = await reader.read();
|
|
4812
|
+
buffer += decoder.decode(chunk.value, { stream: !chunk.done });
|
|
4813
|
+
const lines = buffer.split(NDJSON_SEPARATOR);
|
|
4814
|
+
buffer = lines.pop() ?? "";
|
|
4815
|
+
for (const line of lines) this.#dispatch(line);
|
|
4816
|
+
if (chunk.done) break;
|
|
4817
|
+
}
|
|
4818
|
+
controller.signal.removeEventListener("abort", stop);
|
|
4819
|
+
await reader.cancel().catch(() => void 0);
|
|
4820
|
+
} catch (error) {
|
|
4821
|
+
if (isAbort(error)) {
|
|
4822
|
+
if (this.#controller !== controller) continue;
|
|
4823
|
+
return;
|
|
4824
|
+
}
|
|
4825
|
+
} finally {
|
|
4826
|
+
if (this.#controller === controller) this.#controller = void 0;
|
|
4827
|
+
}
|
|
4828
|
+
if (this.#disposed || this.#wanted.size === 0) return;
|
|
4829
|
+
if (superseded) continue;
|
|
4830
|
+
await delay(this.#retryDelayMs);
|
|
4831
|
+
}
|
|
4832
|
+
} finally {
|
|
4833
|
+
this.#running = false;
|
|
4834
|
+
}
|
|
4835
|
+
}
|
|
4836
|
+
/** Route one carrier line to the session that owns it. A line for a session
|
|
4837
|
+
* nobody is watching any more is dropped rather than reviving its lane. */
|
|
4838
|
+
#dispatch(line) {
|
|
4839
|
+
if (line.length === 0) return;
|
|
4840
|
+
let session;
|
|
4841
|
+
try {
|
|
4842
|
+
session = JSON.parse(line).session;
|
|
4843
|
+
} catch {
|
|
4844
|
+
return;
|
|
4845
|
+
}
|
|
4846
|
+
if (typeof session !== "string") return;
|
|
4847
|
+
this.#sources.get(session)?.feed(line);
|
|
4198
4848
|
}
|
|
4199
4849
|
};
|
|
4200
4850
|
//#endregion
|
|
@@ -4628,29 +5278,6 @@ window.__ModuleLoader__.load({
|
|
|
4628
5278
|
});
|
|
4629
5279
|
}
|
|
4630
5280
|
//#endregion
|
|
4631
|
-
//#region src/client/plugin-request.ts
|
|
4632
|
-
/** Deadlines for the plugin's own HTTP routes.
|
|
4633
|
-
*
|
|
4634
|
-
* A browser opens at most six connections to one origin, and a request that
|
|
4635
|
-
* never settles holds one of them for the life of the page. A handful of
|
|
4636
|
-
* those and every plugin route — the projection stream included — stops
|
|
4637
|
-
* answering, which reads as the whole plugin freezing until the Host is
|
|
4638
|
-
* restarted. So no plain request is allowed to wait forever: the deadline is
|
|
4639
|
-
* generous enough that a slow answer still arrives, and short enough that a
|
|
4640
|
-
* wedged one gives its connection back.
|
|
4641
|
-
*
|
|
4642
|
-
* Streaming routes (projection, ask, repository setup) are legitimately
|
|
4643
|
-
* long-lived and carry their own cancellation instead. */
|
|
4644
|
-
/** Reads: the route answers from cache or a short-timeout Git call. */
|
|
4645
|
-
const PLUGIN_READ_TIMEOUT_MS = 3e4;
|
|
4646
|
-
/** Writes: the route may chain several remote Git/gh calls of its own. */
|
|
4647
|
-
const PLUGIN_ACTION_TIMEOUT_MS = 18e4;
|
|
4648
|
-
/** Combine a caller's cancellation with this deadline. */
|
|
4649
|
-
function pluginRequestSignal(timeoutMs, signal) {
|
|
4650
|
-
const timeout = AbortSignal.timeout(timeoutMs);
|
|
4651
|
-
return signal === void 0 ? timeout : AbortSignal.any([signal, timeout]);
|
|
4652
|
-
}
|
|
4653
|
-
//#endregion
|
|
4654
5281
|
//#region src/client/jira-api.ts
|
|
4655
5282
|
var JiraClientError = class extends Error {
|
|
4656
5283
|
code;
|
|
@@ -4663,68 +5290,75 @@ window.__ModuleLoader__.load({
|
|
|
4663
5290
|
function record$5(value) {
|
|
4664
5291
|
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
4665
5292
|
}
|
|
4666
|
-
|
|
4667
|
-
|
|
4668
|
-
|
|
4669
|
-
|
|
4670
|
-
|
|
4671
|
-
|
|
4672
|
-
|
|
4673
|
-
|
|
5293
|
+
/**
|
|
5294
|
+
* Every Jira failure the panels catch is a `JiraClientError`, whatever the
|
|
5295
|
+
* transport threw: `ClaudeHeroRepositoryControls` branches on `code` to tell
|
|
5296
|
+
* 'not-connected' apart from a real outage, and the settings card renders the
|
|
5297
|
+
* message verbatim.
|
|
5298
|
+
*
|
|
5299
|
+
* The routes answer `{ error, message }`, so `message` is already the sentence
|
|
5300
|
+
* to show. A body carrying only a code — a 405, a bad JSON body — used to read
|
|
5301
|
+
* 'Jira is unavailable.' rather than leaking the code as prose, and it still
|
|
5302
|
+
* does. Transport failures (a starved pool, an elapsed budget, an older Host
|
|
5303
|
+
* without the route) carry their own wording and keep it.
|
|
5304
|
+
*/
|
|
5305
|
+
function jiraFailure(cause) {
|
|
5306
|
+
if (cause instanceof JiraClientError) return cause;
|
|
5307
|
+
if (!(cause instanceof PluginRequestError)) return new JiraClientError(cause instanceof Error ? cause.message : String(cause));
|
|
5308
|
+
return new JiraClientError(cause.message === cause.code ? "Jira is unavailable." : cause.message, cause.code);
|
|
5309
|
+
}
|
|
5310
|
+
function payload(value) {
|
|
5311
|
+
const body = record$5(value);
|
|
4674
5312
|
if (body === void 0) throw new JiraClientError("Invalid Jira response.");
|
|
4675
5313
|
return body;
|
|
4676
5314
|
}
|
|
4677
|
-
function status(
|
|
5315
|
+
function status(value) {
|
|
5316
|
+
const body = payload(value);
|
|
4678
5317
|
if (typeof body.connected !== "boolean") throw new JiraClientError("Invalid Jira status response.");
|
|
4679
5318
|
return body;
|
|
4680
5319
|
}
|
|
4681
5320
|
async function loadJiraStatus(signal) {
|
|
4682
|
-
|
|
4683
|
-
|
|
4684
|
-
|
|
4685
|
-
|
|
4686
|
-
}
|
|
5321
|
+
try {
|
|
5322
|
+
return status(await pluginRead(`${CLAUDE_JIRA_PATH}/status`, "remote", signal));
|
|
5323
|
+
} catch (cause) {
|
|
5324
|
+
throw jiraFailure(cause);
|
|
5325
|
+
}
|
|
4687
5326
|
}
|
|
4688
5327
|
async function connectJira(input) {
|
|
4689
|
-
|
|
4690
|
-
|
|
4691
|
-
|
|
4692
|
-
|
|
4693
|
-
|
|
4694
|
-
},
|
|
4695
|
-
body: JSON.stringify(input)
|
|
4696
|
-
}));
|
|
5328
|
+
try {
|
|
5329
|
+
return status(await pluginWrite(`${CLAUDE_JIRA_PATH}/connect`, "remote", void 0, { json: input }));
|
|
5330
|
+
} catch (cause) {
|
|
5331
|
+
throw jiraFailure(cause);
|
|
5332
|
+
}
|
|
4697
5333
|
}
|
|
4698
5334
|
async function disconnectJira() {
|
|
4699
|
-
|
|
4700
|
-
|
|
4701
|
-
|
|
4702
|
-
|
|
5335
|
+
try {
|
|
5336
|
+
await pluginWrite(`${CLAUDE_JIRA_PATH}/disconnect`, "remote");
|
|
5337
|
+
} catch (cause) {
|
|
5338
|
+
throw jiraFailure(cause);
|
|
5339
|
+
}
|
|
4703
5340
|
}
|
|
4704
5341
|
async function searchJiraTickets(query, signal) {
|
|
4705
|
-
|
|
4706
|
-
|
|
4707
|
-
|
|
4708
|
-
|
|
4709
|
-
|
|
4710
|
-
|
|
4711
|
-
|
|
4712
|
-
|
|
4713
|
-
|
|
4714
|
-
|
|
4715
|
-
|
|
5342
|
+
try {
|
|
5343
|
+
const body = payload(await pluginRead(`${CLAUDE_JIRA_PATH}/search`, "remote", signal, { query: { query } }));
|
|
5344
|
+
if (!Array.isArray(body.tickets)) throw new JiraClientError("Invalid Jira search response.");
|
|
5345
|
+
const tickets = [];
|
|
5346
|
+
for (const item of body.tickets) {
|
|
5347
|
+
const ticket = record$5(item);
|
|
5348
|
+
if (ticket === void 0 || typeof ticket.key !== "string" || typeof ticket.summary !== "string" || typeof ticket.url !== "string") continue;
|
|
5349
|
+
tickets.push(ticket);
|
|
5350
|
+
}
|
|
5351
|
+
return tickets;
|
|
5352
|
+
} catch (cause) {
|
|
5353
|
+
throw jiraFailure(cause);
|
|
4716
5354
|
}
|
|
4717
|
-
return tickets;
|
|
4718
5355
|
}
|
|
4719
5356
|
async function assignJiraTicket(key) {
|
|
4720
|
-
|
|
4721
|
-
|
|
4722
|
-
|
|
4723
|
-
|
|
4724
|
-
|
|
4725
|
-
},
|
|
4726
|
-
body: JSON.stringify({ key })
|
|
4727
|
-
});
|
|
5357
|
+
try {
|
|
5358
|
+
await pluginWrite(`${CLAUDE_JIRA_PATH}/assign`, "remote", void 0, { json: { key } });
|
|
5359
|
+
} catch (cause) {
|
|
5360
|
+
throw jiraFailure(cause);
|
|
5361
|
+
}
|
|
4728
5362
|
}
|
|
4729
5363
|
/** Draft seeded into the composer when a session starts from a ticket. */
|
|
4730
5364
|
function ticketPrompt(ticket) {
|
|
@@ -4761,6 +5395,7 @@ window.__ModuleLoader__.load({
|
|
|
4761
5395
|
const item = setting;
|
|
4762
5396
|
if (typeof item.key !== "string" || typeof item.value !== "string" || ![
|
|
4763
5397
|
"new-session",
|
|
5398
|
+
"next-turn",
|
|
4764
5399
|
"next-worktree",
|
|
4765
5400
|
"restart"
|
|
4766
5401
|
].includes(String(item.effect))) return false;
|
|
@@ -4775,6 +5410,27 @@ window.__ModuleLoader__.load({
|
|
|
4775
5410
|
function value(status, detail) {
|
|
4776
5411
|
return detail === void 0 ? status : `${status} · ${detail}`;
|
|
4777
5412
|
}
|
|
5413
|
+
/** The trigger's disclosure chevron.
|
|
5414
|
+
*
|
|
5415
|
+
* Geometry rather than a character: the ink spans y 6 to 10 in a 16-unit box,
|
|
5416
|
+
* so it is centred on the box's own centre and the open state's 180-degree
|
|
5417
|
+
* flip lands exactly where the closed state sat. A text arrowhead carries its
|
|
5418
|
+
* ink below the centre of the em box, which is why this needed a hand-tuned
|
|
5419
|
+
* nudge that could only be right in one of the two states. */
|
|
5420
|
+
function SelectChevron() {
|
|
5421
|
+
return /* @__PURE__ */ jsx("svg", {
|
|
5422
|
+
width: "16",
|
|
5423
|
+
height: "16",
|
|
5424
|
+
viewBox: "0 0 16 16",
|
|
5425
|
+
fill: "none",
|
|
5426
|
+
stroke: "currentColor",
|
|
5427
|
+
strokeWidth: "1.8",
|
|
5428
|
+
strokeLinecap: "round",
|
|
5429
|
+
strokeLinejoin: "round",
|
|
5430
|
+
"aria-hidden": "true",
|
|
5431
|
+
children: /* @__PURE__ */ jsx("path", { d: "M4 6l4 4 4-4" })
|
|
5432
|
+
});
|
|
5433
|
+
}
|
|
4778
5434
|
function GlobalSettingText({ setting, disabled, onChange }) {
|
|
4779
5435
|
const [draft, setDraft] = useState(setting.value);
|
|
4780
5436
|
useEffect(() => {
|
|
@@ -4805,7 +5461,7 @@ window.__ModuleLoader__.load({
|
|
|
4805
5461
|
}
|
|
4806
5462
|
});
|
|
4807
5463
|
}
|
|
4808
|
-
function GlobalSettingSelect({ setting, disabled, onChange }) {
|
|
5464
|
+
function GlobalSettingSelect({ setting, disabled, onChange, labelFor = (option) => option.label }) {
|
|
4809
5465
|
const [open, setOpen] = useState(false);
|
|
4810
5466
|
const [activeIndex, setActiveIndex] = useState(0);
|
|
4811
5467
|
const rootRef = useRef(null);
|
|
@@ -4843,80 +5499,81 @@ window.__ModuleLoader__.load({
|
|
|
4843
5499
|
onBlur: (event) => {
|
|
4844
5500
|
if (!event.currentTarget.contains(event.relatedTarget)) setOpen(false);
|
|
4845
5501
|
},
|
|
4846
|
-
children: [
|
|
4847
|
-
|
|
4848
|
-
|
|
4849
|
-
|
|
4850
|
-
|
|
4851
|
-
|
|
4852
|
-
|
|
4853
|
-
|
|
4854
|
-
|
|
4855
|
-
|
|
4856
|
-
|
|
4857
|
-
|
|
4858
|
-
|
|
4859
|
-
|
|
4860
|
-
|
|
4861
|
-
|
|
4862
|
-
|
|
4863
|
-
if (event.key === "ArrowDown" || event.key === "ArrowUp") {
|
|
4864
|
-
event.preventDefault();
|
|
4865
|
-
if (!open) openMenu(event.key === "ArrowDown" ? selectedIndex : Math.max(0, setting.options.length - 1));
|
|
4866
|
-
else move(event.key === "ArrowDown" ? 1 : -1);
|
|
4867
|
-
} else if (event.key === "Home" && open) {
|
|
4868
|
-
event.preventDefault();
|
|
4869
|
-
setActiveIndex(0);
|
|
4870
|
-
} else if (event.key === "End" && open) {
|
|
4871
|
-
event.preventDefault();
|
|
4872
|
-
setActiveIndex(Math.max(0, setting.options.length - 1));
|
|
4873
|
-
} else if ((event.key === "Enter" || event.key === " ") && open) {
|
|
4874
|
-
event.preventDefault();
|
|
4875
|
-
choose(activeIndex);
|
|
4876
|
-
} else if (event.key === "Escape" && open) {
|
|
4877
|
-
event.preventDefault();
|
|
4878
|
-
setOpen(false);
|
|
4879
|
-
}
|
|
4880
|
-
},
|
|
4881
|
-
children: [/* @__PURE__ */ jsx("span", {
|
|
4882
|
-
style: settingSelectValue,
|
|
4883
|
-
children: selectedOption?.label ?? setting.value
|
|
4884
|
-
}), /* @__PURE__ */ jsx("span", {
|
|
4885
|
-
"aria-hidden": "true",
|
|
4886
|
-
style: {
|
|
4887
|
-
...settingSelectChevron,
|
|
4888
|
-
...open ? settingSelectChevronOpen : {}
|
|
5502
|
+
children: [
|
|
5503
|
+
/* @__PURE__ */ jsx("style", {
|
|
5504
|
+
"data-dsh-claude-setting-select-styles": true,
|
|
5505
|
+
children: settingSelectCss
|
|
5506
|
+
}),
|
|
5507
|
+
/* @__PURE__ */ jsxs("button", {
|
|
5508
|
+
ref: triggerRef,
|
|
5509
|
+
type: "button",
|
|
5510
|
+
"aria-haspopup": "listbox",
|
|
5511
|
+
"aria-expanded": open,
|
|
5512
|
+
"aria-controls": open ? listboxId : void 0,
|
|
5513
|
+
"aria-activedescendant": open ? `${listboxId}-${activeIndex}` : void 0,
|
|
5514
|
+
disabled: disabled || setting.options.length === 0,
|
|
5515
|
+
className: settingSelectTriggerClass,
|
|
5516
|
+
onClick: () => {
|
|
5517
|
+
if (open) setOpen(false);
|
|
5518
|
+
else openMenu();
|
|
4889
5519
|
},
|
|
4890
|
-
|
|
4891
|
-
|
|
4892
|
-
|
|
4893
|
-
|
|
4894
|
-
|
|
4895
|
-
|
|
4896
|
-
|
|
4897
|
-
|
|
4898
|
-
|
|
4899
|
-
|
|
4900
|
-
|
|
4901
|
-
|
|
4902
|
-
|
|
4903
|
-
|
|
4904
|
-
"
|
|
4905
|
-
|
|
4906
|
-
|
|
4907
|
-
|
|
4908
|
-
|
|
4909
|
-
|
|
4910
|
-
|
|
4911
|
-
|
|
4912
|
-
|
|
4913
|
-
|
|
4914
|
-
|
|
4915
|
-
|
|
4916
|
-
|
|
4917
|
-
|
|
4918
|
-
|
|
4919
|
-
|
|
5520
|
+
onKeyDown: (event) => {
|
|
5521
|
+
if (event.key === "ArrowDown" || event.key === "ArrowUp") {
|
|
5522
|
+
event.preventDefault();
|
|
5523
|
+
if (!open) openMenu(event.key === "ArrowDown" ? selectedIndex : Math.max(0, setting.options.length - 1));
|
|
5524
|
+
else move(event.key === "ArrowDown" ? 1 : -1);
|
|
5525
|
+
} else if (event.key === "Home" && open) {
|
|
5526
|
+
event.preventDefault();
|
|
5527
|
+
setActiveIndex(0);
|
|
5528
|
+
} else if (event.key === "End" && open) {
|
|
5529
|
+
event.preventDefault();
|
|
5530
|
+
setActiveIndex(Math.max(0, setting.options.length - 1));
|
|
5531
|
+
} else if ((event.key === "Enter" || event.key === " ") && open) {
|
|
5532
|
+
event.preventDefault();
|
|
5533
|
+
choose(activeIndex);
|
|
5534
|
+
} else if (event.key === "Escape" && open) {
|
|
5535
|
+
event.preventDefault();
|
|
5536
|
+
setOpen(false);
|
|
5537
|
+
}
|
|
5538
|
+
},
|
|
5539
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
5540
|
+
style: settingSelectValue,
|
|
5541
|
+
children: selectedOption === void 0 ? setting.value : labelFor(selectedOption)
|
|
5542
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
5543
|
+
"aria-hidden": "true",
|
|
5544
|
+
className: settingSelectChevronClass,
|
|
5545
|
+
children: /* @__PURE__ */ jsx(SelectChevron, {})
|
|
5546
|
+
})]
|
|
5547
|
+
}),
|
|
5548
|
+
open ? /* @__PURE__ */ jsx("div", {
|
|
5549
|
+
id: listboxId,
|
|
5550
|
+
role: "listbox",
|
|
5551
|
+
"aria-activedescendant": `${listboxId}-${activeIndex}`,
|
|
5552
|
+
style: settingSelectMenu,
|
|
5553
|
+
children: setting.options.map((option, index) => {
|
|
5554
|
+
const selected = option.value === setting.value;
|
|
5555
|
+
const active = index === activeIndex;
|
|
5556
|
+
return /* @__PURE__ */ jsxs("button", {
|
|
5557
|
+
id: `${listboxId}-${index}`,
|
|
5558
|
+
type: "button",
|
|
5559
|
+
role: "option",
|
|
5560
|
+
"aria-selected": selected,
|
|
5561
|
+
style: {
|
|
5562
|
+
...settingSelectOption,
|
|
5563
|
+
...active ? settingSelectOptionActive : {}
|
|
5564
|
+
},
|
|
5565
|
+
onMouseEnter: () => setActiveIndex(index),
|
|
5566
|
+
onMouseDown: (event) => event.preventDefault(),
|
|
5567
|
+
onClick: () => choose(index),
|
|
5568
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
5569
|
+
style: settingSelectCheck,
|
|
5570
|
+
"aria-hidden": "true",
|
|
5571
|
+
children: selected ? "✓" : ""
|
|
5572
|
+
}), /* @__PURE__ */ jsx("span", { children: labelFor(option) })]
|
|
5573
|
+
}, `${option.source}:${option.value}`);
|
|
5574
|
+
})
|
|
5575
|
+
}) : null
|
|
5576
|
+
]
|
|
4920
5577
|
});
|
|
4921
5578
|
}
|
|
4922
5579
|
/** Fixed windows carry a translated label; server-named model buckets (e.g.
|
|
@@ -4934,6 +5591,10 @@ window.__ModuleLoader__.load({
|
|
|
4934
5591
|
label: "outputStyle",
|
|
4935
5592
|
hint: "globalSettingsNewSession"
|
|
4936
5593
|
},
|
|
5594
|
+
renderer: {
|
|
5595
|
+
label: "renderer",
|
|
5596
|
+
hint: "rendererEffect"
|
|
5597
|
+
},
|
|
4937
5598
|
worktreeBranchPrefix: {
|
|
4938
5599
|
label: "worktreeBranchPrefix",
|
|
4939
5600
|
hint: "worktreeBranchPrefixEffect"
|
|
@@ -4947,6 +5608,17 @@ window.__ModuleLoader__.load({
|
|
|
4947
5608
|
hint: "idleTimeoutEffect"
|
|
4948
5609
|
}
|
|
4949
5610
|
};
|
|
5611
|
+
/** Translated display text for the option vocabularies this plugin owns,
|
|
5612
|
+
* keyed `<setting>:<option>`. Options discovered on the machine (output style
|
|
5613
|
+
* names) carry no entry and keep the label the route reported. */
|
|
5614
|
+
const SETTING_OPTION_COPY = {
|
|
5615
|
+
"renderer:plugin": "rendererPlugin",
|
|
5616
|
+
"renderer:native": "rendererNative"
|
|
5617
|
+
};
|
|
5618
|
+
function settingOptionLabel(settingKey, option, t) {
|
|
5619
|
+
const key = SETTING_OPTION_COPY[`${settingKey}:${option.value}`];
|
|
5620
|
+
return key === void 0 ? option.label : t(key);
|
|
5621
|
+
}
|
|
4950
5622
|
/** '?' badge that reveals a setting's effect note on hover or keyboard focus. */
|
|
4951
5623
|
function SettingHint({ text, label }) {
|
|
4952
5624
|
return /* @__PURE__ */ jsx(Tooltip, {
|
|
@@ -4968,8 +5640,9 @@ window.__ModuleLoader__.load({
|
|
|
4968
5640
|
const report = value;
|
|
4969
5641
|
return typeof report.available === "boolean" && typeof report.fetchedAt === "number" && Array.isArray(report.windows) && report.windows.every((entry) => typeof entry === "object" && entry !== null && typeof entry.id === "string");
|
|
4970
5642
|
}
|
|
4971
|
-
/** Coarse duration for a reset countdown or a
|
|
4972
|
-
* hour, then hours, then days. Never negative — a passed reset
|
|
5643
|
+
/** Coarse duration for a reset countdown or the age of a reading: minutes
|
|
5644
|
+
* below an hour, then hours, then days. Never negative — a passed reset
|
|
5645
|
+
* reads '0m'. */
|
|
4973
5646
|
function durationLabel(milliseconds) {
|
|
4974
5647
|
const minutes = Math.max(0, Math.round(milliseconds / 6e4));
|
|
4975
5648
|
if (minutes < 60) return `${minutes}m`;
|
|
@@ -4999,23 +5672,31 @@ window.__ModuleLoader__.load({
|
|
|
4999
5672
|
})]
|
|
5000
5673
|
});
|
|
5001
5674
|
}
|
|
5002
|
-
|
|
5003
|
-
|
|
5004
|
-
|
|
5005
|
-
|
|
5006
|
-
|
|
5007
|
-
|
|
5008
|
-
|
|
5009
|
-
|
|
5010
|
-
|
|
5011
|
-
|
|
5012
|
-
|
|
5675
|
+
function cardFailure(cause) {
|
|
5676
|
+
return {
|
|
5677
|
+
detail: cause instanceof Error ? cause.message : String(cause),
|
|
5678
|
+
starved: cause instanceof PluginRequestError && cause.reason === "starved"
|
|
5679
|
+
};
|
|
5680
|
+
}
|
|
5681
|
+
function FailureNotice({ label, failure }) {
|
|
5682
|
+
return /* @__PURE__ */ jsxs("p", {
|
|
5683
|
+
role: "alert",
|
|
5684
|
+
style: {
|
|
5685
|
+
...notice,
|
|
5686
|
+
color: failure.starved ? "var(--dsw-alias-state-warning-primary, #d69e2e)" : "var(--dsw-alias-state-error-primary)"
|
|
5687
|
+
},
|
|
5688
|
+
children: [
|
|
5689
|
+
label,
|
|
5690
|
+
": ",
|
|
5691
|
+
failure.detail
|
|
5692
|
+
]
|
|
5013
5693
|
});
|
|
5014
|
-
|
|
5015
|
-
|
|
5016
|
-
|
|
5017
|
-
|
|
5018
|
-
|
|
5694
|
+
}
|
|
5695
|
+
/** The plan usage report. A plain read serves the cached reading from memory;
|
|
5696
|
+
* the refresh spawns a probe process, which is why only it pays the remote
|
|
5697
|
+
* budget. */
|
|
5698
|
+
async function loadPlanUsage(refresh) {
|
|
5699
|
+
const payload = refresh ? await pluginWrite(CLAUDE_USAGE_PATH, "remote") : await pluginRead(CLAUDE_USAGE_PATH, "fast");
|
|
5019
5700
|
if (!isPlanUsageReport(payload)) throw new Error("Invalid plan usage response");
|
|
5020
5701
|
return payload;
|
|
5021
5702
|
}
|
|
@@ -5029,7 +5710,7 @@ window.__ModuleLoader__.load({
|
|
|
5029
5710
|
try {
|
|
5030
5711
|
setReport(await load(refresh));
|
|
5031
5712
|
} catch (cause) {
|
|
5032
|
-
setError(
|
|
5713
|
+
setError(cardFailure(cause));
|
|
5033
5714
|
} finally {
|
|
5034
5715
|
setBusy(false);
|
|
5035
5716
|
}
|
|
@@ -5086,17 +5767,9 @@ window.__ModuleLoader__.load({
|
|
|
5086
5767
|
style: notice,
|
|
5087
5768
|
children: t("planUsageUpdated", { age: durationLabel(now - report.fetchedAt) })
|
|
5088
5769
|
}) : null,
|
|
5089
|
-
error === void 0 ? null : /* @__PURE__ */
|
|
5090
|
-
|
|
5091
|
-
|
|
5092
|
-
...notice,
|
|
5093
|
-
color: "var(--dsw-alias-state-error-primary)"
|
|
5094
|
-
},
|
|
5095
|
-
children: [
|
|
5096
|
-
t("planUsageError"),
|
|
5097
|
-
": ",
|
|
5098
|
-
error
|
|
5099
|
-
]
|
|
5770
|
+
error === void 0 ? null : /* @__PURE__ */ jsx(FailureNotice, {
|
|
5771
|
+
label: t("planUsageError"),
|
|
5772
|
+
failure: error
|
|
5100
5773
|
})
|
|
5101
5774
|
]
|
|
5102
5775
|
});
|
|
@@ -5120,7 +5793,7 @@ window.__ModuleLoader__.load({
|
|
|
5120
5793
|
useEffect(() => {
|
|
5121
5794
|
const controller = new AbortController();
|
|
5122
5795
|
loadJiraStatus(controller.signal).then(setJiraStatus, (reason) => {
|
|
5123
|
-
if (!controller.signal.aborted) setJiraError(
|
|
5796
|
+
if (!controller.signal.aborted) setJiraError(cardFailure(reason));
|
|
5124
5797
|
});
|
|
5125
5798
|
return () => {
|
|
5126
5799
|
controller.abort();
|
|
@@ -5137,7 +5810,7 @@ window.__ModuleLoader__.load({
|
|
|
5137
5810
|
}));
|
|
5138
5811
|
setJiraToken("");
|
|
5139
5812
|
} catch (cause) {
|
|
5140
|
-
setJiraError(
|
|
5813
|
+
setJiraError(cardFailure(cause));
|
|
5141
5814
|
} finally {
|
|
5142
5815
|
setJiraBusy(false);
|
|
5143
5816
|
}
|
|
@@ -5149,7 +5822,7 @@ window.__ModuleLoader__.load({
|
|
|
5149
5822
|
await disconnectJira();
|
|
5150
5823
|
setJiraStatus({ connected: false });
|
|
5151
5824
|
} catch (cause) {
|
|
5152
|
-
setJiraError(
|
|
5825
|
+
setJiraError(cardFailure(cause));
|
|
5153
5826
|
} finally {
|
|
5154
5827
|
setJiraBusy(false);
|
|
5155
5828
|
}
|
|
@@ -5159,16 +5832,9 @@ window.__ModuleLoader__.load({
|
|
|
5159
5832
|
setError(void 0);
|
|
5160
5833
|
setReport(void 0);
|
|
5161
5834
|
try {
|
|
5162
|
-
|
|
5163
|
-
credentials: "same-origin",
|
|
5164
|
-
signal: AbortSignal.timeout(SETTINGS_REQUEST_TIMEOUT_MS),
|
|
5165
|
-
headers: { accept: "application/json" }
|
|
5166
|
-
});
|
|
5167
|
-
const payload = await response.json();
|
|
5168
|
-
if (!response.ok) throw new Error("error" in payload ? payload.error : `HTTP ${response.status}`);
|
|
5169
|
-
setReport(payload);
|
|
5835
|
+
setReport(await pluginRead(CLAUDE_DOCTOR_PATH, "fast"));
|
|
5170
5836
|
} catch (cause) {
|
|
5171
|
-
setError(
|
|
5837
|
+
setError(cardFailure(cause));
|
|
5172
5838
|
} finally {
|
|
5173
5839
|
setBusy(false);
|
|
5174
5840
|
}
|
|
@@ -5180,25 +5846,14 @@ window.__ModuleLoader__.load({
|
|
|
5180
5846
|
setGlobalSettingsBusy(true);
|
|
5181
5847
|
setGlobalSettingsError(void 0);
|
|
5182
5848
|
try {
|
|
5183
|
-
const
|
|
5184
|
-
method:
|
|
5185
|
-
|
|
5186
|
-
signal: AbortSignal.timeout(SETTINGS_REQUEST_TIMEOUT_MS),
|
|
5187
|
-
headers: {
|
|
5188
|
-
accept: "application/json",
|
|
5189
|
-
...changes === void 0 ? {} : { "content-type": "application/json" }
|
|
5190
|
-
},
|
|
5191
|
-
...changes === void 0 ? {} : { body: JSON.stringify({ changes }) }
|
|
5849
|
+
const payload = changes === void 0 ? await pluginRead(CLAUDE_GLOBAL_SETTINGS_PATH, "fast") : await pluginWrite(CLAUDE_GLOBAL_SETTINGS_PATH, "fast", void 0, {
|
|
5850
|
+
method: "PATCH",
|
|
5851
|
+
json: { changes }
|
|
5192
5852
|
});
|
|
5193
|
-
const payload = await response.json();
|
|
5194
|
-
if (!response.ok) {
|
|
5195
|
-
const message = typeof payload === "object" && payload !== null && "error" in payload && typeof payload.error === "string" ? payload.error : `HTTP ${response.status}`;
|
|
5196
|
-
throw new Error(message);
|
|
5197
|
-
}
|
|
5198
5853
|
if (!isGlobalSettingsView(payload)) throw new Error("Invalid global settings response");
|
|
5199
5854
|
setGlobalSettings(payload);
|
|
5200
5855
|
} catch (cause) {
|
|
5201
|
-
setGlobalSettingsError(
|
|
5856
|
+
setGlobalSettingsError(cardFailure(cause));
|
|
5202
5857
|
} finally {
|
|
5203
5858
|
setGlobalSettingsBusy(false);
|
|
5204
5859
|
}
|
|
@@ -5210,20 +5865,11 @@ window.__ModuleLoader__.load({
|
|
|
5210
5865
|
setUpdateBusy(action);
|
|
5211
5866
|
setUpdateError(void 0);
|
|
5212
5867
|
try {
|
|
5213
|
-
const
|
|
5214
|
-
method: action === "check" ? "GET" : "POST",
|
|
5215
|
-
credentials: "same-origin",
|
|
5216
|
-
headers: { accept: "application/json" }
|
|
5217
|
-
});
|
|
5218
|
-
const payload = await response.json();
|
|
5219
|
-
if (!response.ok) {
|
|
5220
|
-
const message = typeof payload === "object" && payload !== null && "error" in payload && typeof payload.error === "string" ? payload.error : `HTTP ${response.status}`;
|
|
5221
|
-
throw new Error(message);
|
|
5222
|
-
}
|
|
5868
|
+
const payload = action === "check" ? await pluginRead(CLAUDE_UPDATE_CHECK_PATH, "remote") : await pluginWrite(CLAUDE_UPDATE_PATH, "remote");
|
|
5223
5869
|
if (!isPluginUpdateStatus(payload)) throw new Error("Invalid update response");
|
|
5224
5870
|
setUpdateStatus(payload);
|
|
5225
5871
|
} catch (cause) {
|
|
5226
|
-
setUpdateError(
|
|
5872
|
+
setUpdateError(cardFailure(cause));
|
|
5227
5873
|
} finally {
|
|
5228
5874
|
setUpdateBusy(void 0);
|
|
5229
5875
|
}
|
|
@@ -5293,17 +5939,9 @@ window.__ModuleLoader__.load({
|
|
|
5293
5939
|
children: rowValue
|
|
5294
5940
|
}, `${label}-value`)])
|
|
5295
5941
|
}),
|
|
5296
|
-
error === void 0 ? null : /* @__PURE__ */
|
|
5297
|
-
|
|
5298
|
-
|
|
5299
|
-
...notice,
|
|
5300
|
-
color: "var(--dsw-alias-state-error-primary)"
|
|
5301
|
-
},
|
|
5302
|
-
children: [
|
|
5303
|
-
t("error"),
|
|
5304
|
-
": ",
|
|
5305
|
-
error
|
|
5306
|
-
]
|
|
5942
|
+
error === void 0 ? null : /* @__PURE__ */ jsx(FailureNotice, {
|
|
5943
|
+
label: t("error"),
|
|
5944
|
+
failure: error
|
|
5307
5945
|
})
|
|
5308
5946
|
]
|
|
5309
5947
|
}),
|
|
@@ -5337,6 +5975,7 @@ window.__ModuleLoader__.load({
|
|
|
5337
5975
|
}), setting.kind === "select" ? /* @__PURE__ */ jsx(GlobalSettingSelect, {
|
|
5338
5976
|
setting,
|
|
5339
5977
|
disabled: globalSettingsBusy,
|
|
5978
|
+
labelFor: (option) => settingOptionLabel(setting.key, option, t),
|
|
5340
5979
|
onChange: (nextValue) => {
|
|
5341
5980
|
requestGlobalSettings({ [setting.key]: nextValue });
|
|
5342
5981
|
}
|
|
@@ -5349,17 +5988,9 @@ window.__ModuleLoader__.load({
|
|
|
5349
5988
|
})]
|
|
5350
5989
|
}, setting.key);
|
|
5351
5990
|
}),
|
|
5352
|
-
globalSettingsError === void 0 ? null : /* @__PURE__ */
|
|
5353
|
-
|
|
5354
|
-
|
|
5355
|
-
...notice,
|
|
5356
|
-
color: "var(--dsw-alias-state-error-primary)"
|
|
5357
|
-
},
|
|
5358
|
-
children: [
|
|
5359
|
-
t("globalSettingsError"),
|
|
5360
|
-
": ",
|
|
5361
|
-
globalSettingsError
|
|
5362
|
-
]
|
|
5991
|
+
globalSettingsError === void 0 ? null : /* @__PURE__ */ jsx(FailureNotice, {
|
|
5992
|
+
label: t("globalSettingsError"),
|
|
5993
|
+
failure: globalSettingsError
|
|
5363
5994
|
})
|
|
5364
5995
|
]
|
|
5365
5996
|
}),
|
|
@@ -5465,17 +6096,9 @@ window.__ModuleLoader__.load({
|
|
|
5465
6096
|
})]
|
|
5466
6097
|
})
|
|
5467
6098
|
] }),
|
|
5468
|
-
jiraError === void 0 ? null : /* @__PURE__ */
|
|
5469
|
-
|
|
5470
|
-
|
|
5471
|
-
...notice,
|
|
5472
|
-
color: "var(--dsw-alias-state-error-primary)"
|
|
5473
|
-
},
|
|
5474
|
-
children: [
|
|
5475
|
-
t("jiraError"),
|
|
5476
|
-
": ",
|
|
5477
|
-
jiraError
|
|
5478
|
-
]
|
|
6099
|
+
jiraError === void 0 ? null : /* @__PURE__ */ jsx(FailureNotice, {
|
|
6100
|
+
label: t("jiraError"),
|
|
6101
|
+
failure: jiraError
|
|
5479
6102
|
})
|
|
5480
6103
|
]
|
|
5481
6104
|
}),
|
|
@@ -5536,17 +6159,9 @@ window.__ModuleLoader__.load({
|
|
|
5536
6159
|
style: notice,
|
|
5537
6160
|
children: t("restartRequired")
|
|
5538
6161
|
}),
|
|
5539
|
-
updateError === void 0 ? null : /* @__PURE__ */
|
|
5540
|
-
|
|
5541
|
-
|
|
5542
|
-
...notice,
|
|
5543
|
-
color: "var(--dsw-alias-state-error-primary)"
|
|
5544
|
-
},
|
|
5545
|
-
children: [
|
|
5546
|
-
t("updateError"),
|
|
5547
|
-
": ",
|
|
5548
|
-
updateError
|
|
5549
|
-
]
|
|
6162
|
+
updateError === void 0 ? null : /* @__PURE__ */ jsx(FailureNotice, {
|
|
6163
|
+
label: t("updateError"),
|
|
6164
|
+
failure: updateError
|
|
5550
6165
|
}),
|
|
5551
6166
|
/* @__PURE__ */ jsxs("div", {
|
|
5552
6167
|
style: settingsActions,
|
|
@@ -5598,14 +6213,14 @@ window.__ModuleLoader__.load({
|
|
|
5598
6213
|
function record$4(value) {
|
|
5599
6214
|
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
5600
6215
|
}
|
|
5601
|
-
|
|
5602
|
-
|
|
5603
|
-
|
|
5604
|
-
|
|
5605
|
-
|
|
5606
|
-
|
|
5607
|
-
|
|
5608
|
-
return
|
|
6216
|
+
/** The dialog branches on `code`, so a route refusal keeps arriving as this
|
|
6217
|
+
* class rather than as the transport's own error.
|
|
6218
|
+
*
|
|
6219
|
+
* `commit` cannot be carried across: the transport forwards a failed route's
|
|
6220
|
+
* message and error code, not the rest of its body, so a commit that survived
|
|
6221
|
+
* a failed push no longer reaches the dialog that offers its hash. */
|
|
6222
|
+
function actionError(error) {
|
|
6223
|
+
return error instanceof PluginRequestError ? new RepositoryActionClientError(error.message, error.code) : error;
|
|
5609
6224
|
}
|
|
5610
6225
|
function preview(value) {
|
|
5611
6226
|
const input = record$4(value);
|
|
@@ -5625,42 +6240,35 @@ window.__ModuleLoader__.load({
|
|
|
5625
6240
|
if (input === void 0 || typeof input.commit !== "string" || typeof input.pushed !== "boolean" || input.pullRequestUrl !== void 0 && typeof input.pullRequestUrl !== "string" || input.conflicts !== void 0 && (!Array.isArray(input.conflicts) || input.conflicts.some((item) => typeof item !== "string"))) throw new Error("Invalid repository action result.");
|
|
5626
6241
|
return input;
|
|
5627
6242
|
}
|
|
5628
|
-
|
|
5629
|
-
return `${CLAUDE_REPOSITORY_ACTION_PATH}${path}?sessionId=${encodeURIComponent(sessionId)}`;
|
|
5630
|
-
}
|
|
6243
|
+
/** The preview only chains local Git; everything that writes may reach a remote. */
|
|
5631
6244
|
async function loadRepositoryActionPreview(sessionId, signal) {
|
|
5632
|
-
|
|
5633
|
-
|
|
5634
|
-
|
|
5635
|
-
|
|
5636
|
-
|
|
5637
|
-
})));
|
|
6245
|
+
try {
|
|
6246
|
+
return preview(await pluginRead(`${CLAUDE_REPOSITORY_ACTION_PATH}/preview`, "git", signal, { query: { sessionId } }));
|
|
6247
|
+
} catch (error) {
|
|
6248
|
+
throw actionError(error);
|
|
6249
|
+
}
|
|
5638
6250
|
}
|
|
5639
6251
|
async function generateCommitMessage(sessionId, fingerprint, signal) {
|
|
5640
|
-
|
|
5641
|
-
|
|
5642
|
-
|
|
5643
|
-
|
|
5644
|
-
|
|
5645
|
-
|
|
5646
|
-
|
|
5647
|
-
|
|
5648
|
-
|
|
5649
|
-
}
|
|
5650
|
-
|
|
5651
|
-
|
|
5652
|
-
|
|
5653
|
-
|
|
5654
|
-
|
|
5655
|
-
|
|
5656
|
-
|
|
5657
|
-
|
|
5658
|
-
|
|
5659
|
-
|
|
5660
|
-
},
|
|
5661
|
-
body: JSON.stringify(request),
|
|
5662
|
-
signal: pluginRequestSignal(PLUGIN_ACTION_TIMEOUT_MS)
|
|
5663
|
-
})).then(result);
|
|
6252
|
+
try {
|
|
6253
|
+
const value = record$4(await pluginWrite(`${CLAUDE_REPOSITORY_ACTION_PATH}/message`, "remote", signal, {
|
|
6254
|
+
query: { sessionId },
|
|
6255
|
+
json: { fingerprint }
|
|
6256
|
+
}));
|
|
6257
|
+
if (typeof value?.message !== "string") throw new Error("Invalid generated commit message.");
|
|
6258
|
+
return value.message;
|
|
6259
|
+
} catch (error) {
|
|
6260
|
+
throw actionError(error);
|
|
6261
|
+
}
|
|
6262
|
+
}
|
|
6263
|
+
async function executeRepositoryAction(sessionId, request) {
|
|
6264
|
+
try {
|
|
6265
|
+
return result(await pluginWrite(CLAUDE_REPOSITORY_ACTION_PATH, "remote", void 0, {
|
|
6266
|
+
query: { sessionId },
|
|
6267
|
+
json: request
|
|
6268
|
+
}));
|
|
6269
|
+
} catch (error) {
|
|
6270
|
+
throw actionError(error);
|
|
6271
|
+
}
|
|
5664
6272
|
}
|
|
5665
6273
|
//#endregion
|
|
5666
6274
|
//#region src/client/action-toast.tsx
|
|
@@ -5722,130 +6330,79 @@ window.__ModuleLoader__.load({
|
|
|
5722
6330
|
if (event?.type === "error" && typeof event.message === "string") throw new Error(event.message);
|
|
5723
6331
|
throw new Error("Invalid repository setup progress response.");
|
|
5724
6332
|
}
|
|
5725
|
-
async function response(pending) {
|
|
5726
|
-
const result = await pending;
|
|
5727
|
-
const body = await result.json();
|
|
5728
|
-
if (!result.ok) {
|
|
5729
|
-
const error = body;
|
|
5730
|
-
throw new Error(error.message ?? error.error ?? "Repository setup failed.");
|
|
5731
|
-
}
|
|
5732
|
-
return body;
|
|
5733
|
-
}
|
|
5734
|
-
/** A wedged host must surface as an error; the hero has no other way out of its loading state. */
|
|
5735
|
-
const BRANCH_LOAD_TIMEOUT_MS = 15e3;
|
|
5736
6333
|
function loadRepositoryBranches(cwd, signal) {
|
|
5737
|
-
return
|
|
5738
|
-
method: "GET",
|
|
5739
|
-
credentials: "same-origin",
|
|
5740
|
-
headers: { accept: "application/json" },
|
|
5741
|
-
signal: pluginRequestSignal(BRANCH_LOAD_TIMEOUT_MS, signal)
|
|
5742
|
-
}));
|
|
6334
|
+
return pluginRead(`${CLAUDE_REPOSITORY_SETUP_PATH}/branches`, "git", signal, { query: { cwd } });
|
|
5743
6335
|
}
|
|
5744
6336
|
/** Pull remote refs down first, then list: a POST because `--prune` rewrites
|
|
5745
|
-
* this checkout's remote-tracking refs, and on the
|
|
6337
|
+
* this checkout's remote-tracking refs, and on the remote budget because the
|
|
5746
6338
|
* host's own `git fetch` runs for up to a minute. */
|
|
5747
6339
|
async function refreshRepositoryBranches(cwd, signal) {
|
|
5748
|
-
|
|
5749
|
-
|
|
5750
|
-
|
|
5751
|
-
|
|
5752
|
-
|
|
5753
|
-
|
|
5754
|
-
},
|
|
5755
|
-
signal: pluginRequestSignal(PLUGIN_ACTION_TIMEOUT_MS, signal),
|
|
5756
|
-
body: JSON.stringify({ cwd })
|
|
5757
|
-
});
|
|
5758
|
-
if (result.status === 404) throw new Error("route-missing");
|
|
5759
|
-
return response(Promise.resolve(result));
|
|
6340
|
+
try {
|
|
6341
|
+
return await pluginWrite(`${CLAUDE_REPOSITORY_SETUP_PATH}/branches/refresh`, "remote", signal, { json: { cwd } });
|
|
6342
|
+
} catch (error) {
|
|
6343
|
+
if (error instanceof PluginRequestError && error.reason === "route-missing") throw new Error("route-missing");
|
|
6344
|
+
throw error;
|
|
6345
|
+
}
|
|
5760
6346
|
}
|
|
5761
6347
|
async function prepareRepository(cwd, branch, worktree, branchName, onProgress = () => {}) {
|
|
5762
|
-
const
|
|
5763
|
-
|
|
5764
|
-
|
|
5765
|
-
|
|
5766
|
-
|
|
5767
|
-
|
|
5768
|
-
|
|
5769
|
-
|
|
5770
|
-
|
|
5771
|
-
|
|
5772
|
-
|
|
5773
|
-
|
|
5774
|
-
|
|
5775
|
-
|
|
5776
|
-
|
|
5777
|
-
|
|
5778
|
-
|
|
5779
|
-
|
|
5780
|
-
|
|
5781
|
-
|
|
5782
|
-
|
|
5783
|
-
|
|
5784
|
-
|
|
5785
|
-
|
|
5786
|
-
|
|
5787
|
-
|
|
5788
|
-
|
|
5789
|
-
|
|
5790
|
-
for (const line of lines) {
|
|
5791
|
-
if (line.trim().length === 0) continue;
|
|
5792
|
-
const value = parseRepositorySetupEvent(line, onProgress);
|
|
6348
|
+
const carrier = new AbortController();
|
|
6349
|
+
try {
|
|
6350
|
+
const reader = await pluginNdjson(CLAUDE_REPOSITORY_SETUP_PATH, carrier.signal, {
|
|
6351
|
+
method: "POST",
|
|
6352
|
+
json: {
|
|
6353
|
+
cwd,
|
|
6354
|
+
branch,
|
|
6355
|
+
worktree,
|
|
6356
|
+
...branchName === void 0 ? {} : { branchName }
|
|
6357
|
+
}
|
|
6358
|
+
});
|
|
6359
|
+
const decoder = new TextDecoder();
|
|
6360
|
+
let buffer = "";
|
|
6361
|
+
let completed;
|
|
6362
|
+
while (true) {
|
|
6363
|
+
const chunk = await reader.read();
|
|
6364
|
+
buffer += decoder.decode(chunk.value, { stream: !chunk.done });
|
|
6365
|
+
const lines = buffer.split("\n");
|
|
6366
|
+
buffer = lines.pop() ?? "";
|
|
6367
|
+
for (const line of lines) {
|
|
6368
|
+
if (line.trim().length === 0) continue;
|
|
6369
|
+
const value = parseRepositorySetupEvent(line, onProgress);
|
|
6370
|
+
if (value !== void 0) completed = value;
|
|
6371
|
+
}
|
|
6372
|
+
if (chunk.done) break;
|
|
6373
|
+
}
|
|
6374
|
+
if (buffer.trim().length > 0) {
|
|
6375
|
+
const value = parseRepositorySetupEvent(buffer, onProgress);
|
|
5793
6376
|
if (value !== void 0) completed = value;
|
|
5794
6377
|
}
|
|
5795
|
-
if (
|
|
5796
|
-
|
|
5797
|
-
|
|
5798
|
-
|
|
5799
|
-
if (value !== void 0) completed = value;
|
|
6378
|
+
if (completed === void 0) throw new Error("Repository setup progress ended before completion.");
|
|
6379
|
+
return completed;
|
|
6380
|
+
} finally {
|
|
6381
|
+
carrier.abort();
|
|
5800
6382
|
}
|
|
5801
|
-
if (completed === void 0) throw new Error("Repository setup progress ended before completion.");
|
|
5802
|
-
return completed;
|
|
5803
6383
|
}
|
|
5804
|
-
/** Bookkeeping, not a gate: a wedged host must fail this instead of hanging on
|
|
5805
|
-
* a connection the rest of the flow is waiting behind. */
|
|
5806
|
-
const LEASE_BIND_TIMEOUT_MS = 1e4;
|
|
5807
6384
|
async function bindRepositoryLease(leaseId, sessionId) {
|
|
5808
|
-
await
|
|
5809
|
-
|
|
5810
|
-
|
|
5811
|
-
|
|
5812
|
-
headers: {
|
|
5813
|
-
accept: "application/json",
|
|
5814
|
-
"content-type": "application/json"
|
|
5815
|
-
},
|
|
5816
|
-
body: JSON.stringify({
|
|
5817
|
-
leaseId,
|
|
5818
|
-
sessionId
|
|
5819
|
-
})
|
|
5820
|
-
}));
|
|
6385
|
+
await pluginWrite(`${CLAUDE_REPOSITORY_SETUP_PATH}/bind`, "remote", void 0, { json: {
|
|
6386
|
+
leaseId,
|
|
6387
|
+
sessionId
|
|
6388
|
+
} });
|
|
5821
6389
|
}
|
|
5822
6390
|
async function cleanupMergedRepository(path, baseBranch) {
|
|
5823
|
-
const body = await
|
|
5824
|
-
|
|
5825
|
-
|
|
5826
|
-
|
|
5827
|
-
headers: {
|
|
5828
|
-
accept: "application/json",
|
|
5829
|
-
"content-type": "application/json"
|
|
5830
|
-
},
|
|
5831
|
-
body: JSON.stringify({
|
|
5832
|
-
path,
|
|
5833
|
-
baseBranch
|
|
5834
|
-
})
|
|
5835
|
-
}));
|
|
6391
|
+
const body = await pluginWrite(`${CLAUDE_REPOSITORY_SETUP_PATH}/cleanup`, "remote", void 0, { json: {
|
|
6392
|
+
path,
|
|
6393
|
+
baseBranch
|
|
6394
|
+
} });
|
|
5836
6395
|
if (body.mode !== "worktree" && body.mode !== "checkout" || typeof body.root !== "string" || typeof body.branch !== "string") throw new Error("Invalid repository cleanup response.");
|
|
5837
6396
|
return body;
|
|
5838
6397
|
}
|
|
5839
6398
|
/** Lines [from, to] of a working-tree file plus its total line count, for expanding unmodified diff context. */
|
|
5840
6399
|
async function loadRepositoryFileLines(cwd, path, from, to, signal) {
|
|
5841
|
-
const
|
|
5842
|
-
|
|
5843
|
-
|
|
5844
|
-
|
|
5845
|
-
|
|
5846
|
-
|
|
5847
|
-
...signal === void 0 ? {} : { signal }
|
|
5848
|
-
}));
|
|
6400
|
+
const body = await pluginRead(CLAUDE_REPOSITORY_FILE_PATH, "git", signal, { query: {
|
|
6401
|
+
cwd,
|
|
6402
|
+
path,
|
|
6403
|
+
from: String(from),
|
|
6404
|
+
to: String(to)
|
|
6405
|
+
} });
|
|
5849
6406
|
if (!Array.isArray(body.lines) || typeof body.total !== "number") throw new Error("Invalid repository file response.");
|
|
5850
6407
|
return {
|
|
5851
6408
|
lines: body.lines.map(String),
|
|
@@ -5853,13 +6410,7 @@ window.__ModuleLoader__.load({
|
|
|
5853
6410
|
};
|
|
5854
6411
|
}
|
|
5855
6412
|
async function loadRepositoryStatusFor(cwd, signal) {
|
|
5856
|
-
const body = await
|
|
5857
|
-
signal: pluginRequestSignal(PLUGIN_READ_TIMEOUT_MS),
|
|
5858
|
-
method: "GET",
|
|
5859
|
-
credentials: "same-origin",
|
|
5860
|
-
headers: { accept: "application/json" },
|
|
5861
|
-
...signal === void 0 ? {} : { signal }
|
|
5862
|
-
}));
|
|
6413
|
+
const body = await pluginRead(CLAUDE_REPOSITORY_STATUS_PATH, "git", signal, { query: { cwd } });
|
|
5863
6414
|
if (typeof body.status !== "string" || typeof body.cwd !== "string") throw new Error("Invalid repository status response.");
|
|
5864
6415
|
return body;
|
|
5865
6416
|
}
|
|
@@ -5896,34 +6447,27 @@ window.__ModuleLoader__.load({
|
|
|
5896
6447
|
function record$2(value) {
|
|
5897
6448
|
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
5898
6449
|
}
|
|
5899
|
-
function
|
|
5900
|
-
return
|
|
6450
|
+
function feedbackQuery(sessionId, pullNumber, extra) {
|
|
6451
|
+
return {
|
|
6452
|
+
sessionId,
|
|
6453
|
+
number: String(pullNumber),
|
|
6454
|
+
...extra
|
|
6455
|
+
};
|
|
5901
6456
|
}
|
|
5902
|
-
|
|
5903
|
-
const body = record$2(
|
|
5904
|
-
if (!response.ok) throw new Error(typeof body?.message === "string" ? body.message : "Pull request feedback is unavailable.");
|
|
6457
|
+
function answer(value) {
|
|
6458
|
+
const body = record$2(value);
|
|
5905
6459
|
if (body === void 0) throw new Error("Invalid pull request feedback response.");
|
|
5906
6460
|
return body;
|
|
5907
6461
|
}
|
|
5908
|
-
|
|
5909
|
-
|
|
5910
|
-
|
|
5911
|
-
|
|
5912
|
-
headers: { accept: "application/json" },
|
|
5913
|
-
signal: pluginRequestSignal(PLUGIN_READ_TIMEOUT_MS, signal)
|
|
5914
|
-
}));
|
|
6462
|
+
/** Every arm of this route shells out to `gh`, so reads and writes alike take
|
|
6463
|
+
* the remote budget. */
|
|
6464
|
+
async function loadJson(path, sessionId, pullNumber, signal, extra) {
|
|
6465
|
+
return answer(await pluginRead(`${CLAUDE_REPOSITORY_FEEDBACK_PATH}${path}`, "remote", signal, { query: feedbackQuery(sessionId, pullNumber, extra) }));
|
|
5915
6466
|
}
|
|
5916
|
-
/** Writes reach GitHub through `gh`, so they get the action deadline. */
|
|
5917
6467
|
async function postJson(path, sessionId, pullNumber, input) {
|
|
5918
|
-
return answer(await
|
|
5919
|
-
|
|
5920
|
-
|
|
5921
|
-
headers: {
|
|
5922
|
-
accept: "application/json",
|
|
5923
|
-
"content-type": "application/json"
|
|
5924
|
-
},
|
|
5925
|
-
signal: pluginRequestSignal(PLUGIN_ACTION_TIMEOUT_MS),
|
|
5926
|
-
body: JSON.stringify(input)
|
|
6468
|
+
return answer(await pluginWrite(`${CLAUDE_REPOSITORY_FEEDBACK_PATH}${path}`, "remote", void 0, {
|
|
6469
|
+
query: feedbackQuery(sessionId, pullNumber),
|
|
6470
|
+
json: input
|
|
5927
6471
|
}));
|
|
5928
6472
|
}
|
|
5929
6473
|
function reviewComment(value) {
|
|
@@ -5972,7 +6516,7 @@ window.__ModuleLoader__.load({
|
|
|
5972
6516
|
}
|
|
5973
6517
|
/** Logins GitHub would notify, for the reply composer's `@` completion. */
|
|
5974
6518
|
async function loadMentionableUsers(sessionId, pullNumber, query, signal) {
|
|
5975
|
-
const body = await loadJson("/mentionables", sessionId, pullNumber, signal,
|
|
6519
|
+
const body = await loadJson("/mentionables", sessionId, pullNumber, signal, { q: query });
|
|
5976
6520
|
if (!Array.isArray(body.users)) return [];
|
|
5977
6521
|
const users = [];
|
|
5978
6522
|
for (const item of body.users) {
|
|
@@ -7128,22 +7672,10 @@ window.__ModuleLoader__.load({
|
|
|
7128
7672
|
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
7129
7673
|
}
|
|
7130
7674
|
async function post(path, sessionId, body) {
|
|
7131
|
-
|
|
7132
|
-
|
|
7133
|
-
|
|
7134
|
-
headers: {
|
|
7135
|
-
accept: "application/json",
|
|
7136
|
-
"content-type": "application/json"
|
|
7137
|
-
},
|
|
7138
|
-
body: JSON.stringify(body),
|
|
7139
|
-
signal: pluginRequestSignal(PLUGIN_READ_TIMEOUT_MS)
|
|
7675
|
+
return await pluginWrite(`${CLAUDE_REVIEW_COMMENT_PATH}${path}`, "fast", void 0, {
|
|
7676
|
+
query: { sessionId },
|
|
7677
|
+
json: body
|
|
7140
7678
|
});
|
|
7141
|
-
const value = await response.json();
|
|
7142
|
-
if (!response.ok) {
|
|
7143
|
-
const error = record$1(value);
|
|
7144
|
-
throw new Error(typeof error?.message === "string" ? error.message : "Review comment request failed.");
|
|
7145
|
-
}
|
|
7146
|
-
return value;
|
|
7147
7679
|
}
|
|
7148
7680
|
async function addReviewComment(sessionId, comment) {
|
|
7149
7681
|
const created = record$1(record$1(await post("", sessionId, comment))?.comment);
|
|
@@ -13302,6 +13834,16 @@ window.__ModuleLoader__.load({
|
|
|
13302
13834
|
});
|
|
13303
13835
|
}
|
|
13304
13836
|
//#endregion
|
|
13837
|
+
//#region src/client/session-preset.ts
|
|
13838
|
+
/**
|
|
13839
|
+
* Resolve one row's preset id, newest seat first.
|
|
13840
|
+
* @param row - a session-list row, or undefined when the id is not listed.
|
|
13841
|
+
* @returns the preset id, or undefined when neither source carries one.
|
|
13842
|
+
*/
|
|
13843
|
+
function sessionRowPreset(row) {
|
|
13844
|
+
return row?.agentPreset ?? row?.projectionValues?.agentPreset;
|
|
13845
|
+
}
|
|
13846
|
+
//#endregion
|
|
13305
13847
|
//#region src/client/ClaudePullRequestsPanel.tsx
|
|
13306
13848
|
const NO_WORKSPACE_STATE = {};
|
|
13307
13849
|
const NO_WORKSPACES = {
|
|
@@ -13324,7 +13866,7 @@ window.__ModuleLoader__.load({
|
|
|
13324
13866
|
function claudeSessionRows(state, archivedSessionIds = []) {
|
|
13325
13867
|
const rows = state.ids === void 0 ? Object.values(state.byId) : state.ids.map((id) => state.byId[id]);
|
|
13326
13868
|
const archived = new Set(archivedSessionIds);
|
|
13327
|
-
return rows.filter((row) => row !== void 0 && row
|
|
13869
|
+
return rows.filter((row) => row !== void 0 && sessionRowPreset(row) === "claude" && row.blank !== true && row.origin !== "subagent" && !archived.has(row.id) && typeof row.cwd === "string").sort((left, right) => Number(right.running === true) - Number(left.running === true) || (left.displayTitle ?? left.id).localeCompare(right.displayTitle ?? right.id));
|
|
13328
13870
|
}
|
|
13329
13871
|
function Badge({ label, tone = "neutral" }) {
|
|
13330
13872
|
const toneStyle = tone === "success" ? repositoryItemSuccess : tone === "warning" ? repositoryItemWarning : tone === "error" ? repositoryItemError : tone === "merged" ? { color: "#a78bfa" } : {};
|
|
@@ -13519,56 +14061,64 @@ window.__ModuleLoader__.load({
|
|
|
13519
14061
|
};
|
|
13520
14062
|
throw new Error("Invalid ask stream event.");
|
|
13521
14063
|
}
|
|
14064
|
+
async function openAnswer(sessionId, request, cancel) {
|
|
14065
|
+
try {
|
|
14066
|
+
return await pluginNdjson(CLAUDE_ASK_PATH, cancel, {
|
|
14067
|
+
method: "POST",
|
|
14068
|
+
query: { sessionId },
|
|
14069
|
+
json: request
|
|
14070
|
+
});
|
|
14071
|
+
} catch (error) {
|
|
14072
|
+
if (error instanceof PluginRequestError && error.reason === "http") throw new Error("The question could not be sent.");
|
|
14073
|
+
throw error;
|
|
14074
|
+
}
|
|
14075
|
+
}
|
|
13522
14076
|
/** Stream an answer about selected reply text; resolves when the answer completes. */
|
|
13523
14077
|
async function askAboutSelection(sessionId, request, onProgress, signal) {
|
|
13524
|
-
const
|
|
13525
|
-
|
|
13526
|
-
|
|
13527
|
-
headers: {
|
|
13528
|
-
accept: "application/x-ndjson",
|
|
13529
|
-
"content-type": "application/json"
|
|
13530
|
-
},
|
|
13531
|
-
body: JSON.stringify(request),
|
|
13532
|
-
...signal === void 0 ? {} : { signal }
|
|
13533
|
-
});
|
|
13534
|
-
if (!response.ok) {
|
|
13535
|
-
const body = record(await response.json().catch(() => void 0));
|
|
13536
|
-
throw new Error(typeof body?.message === "string" ? body.message : "The question could not be sent.");
|
|
13537
|
-
}
|
|
13538
|
-
if (response.body === null) throw new Error("The answer stream is unavailable.");
|
|
13539
|
-
const reader = response.body.getReader();
|
|
13540
|
-
const decoder = new TextDecoder();
|
|
13541
|
-
let buffer = "";
|
|
13542
|
-
let finished = false;
|
|
13543
|
-
const handle = (line) => {
|
|
13544
|
-
if (line.trim().length === 0) return;
|
|
13545
|
-
const event = parseAskEvent(line);
|
|
13546
|
-
if (event.type === "delta") onProgress({
|
|
13547
|
-
type: "text",
|
|
13548
|
-
text: event.text
|
|
13549
|
-
});
|
|
13550
|
-
else if (event.type === "thinking") onProgress({
|
|
13551
|
-
type: "thinking",
|
|
13552
|
-
text: event.text
|
|
13553
|
-
});
|
|
13554
|
-
else if (event.type === "status") onProgress({
|
|
13555
|
-
type: "status",
|
|
13556
|
-
text: event.text
|
|
13557
|
-
});
|
|
13558
|
-
else if (event.type === "tool") onProgress(event);
|
|
13559
|
-
else if (event.type === "done") finished = true;
|
|
13560
|
-
else throw new Error(event.message);
|
|
14078
|
+
const carrier = new AbortController();
|
|
14079
|
+
const stop = () => {
|
|
14080
|
+
carrier.abort();
|
|
13561
14081
|
};
|
|
13562
|
-
|
|
13563
|
-
|
|
13564
|
-
|
|
13565
|
-
const
|
|
13566
|
-
|
|
13567
|
-
|
|
13568
|
-
|
|
14082
|
+
signal?.addEventListener("abort", stop, { once: true });
|
|
14083
|
+
if (signal?.aborted === true) carrier.abort();
|
|
14084
|
+
try {
|
|
14085
|
+
const reader = await openAnswer(sessionId, request, carrier.signal);
|
|
14086
|
+
const decoder = new TextDecoder();
|
|
14087
|
+
let buffer = "";
|
|
14088
|
+
let finished = false;
|
|
14089
|
+
const handle = (line) => {
|
|
14090
|
+
if (line.trim().length === 0) return;
|
|
14091
|
+
const event = parseAskEvent(line);
|
|
14092
|
+
if (event.type === "delta") onProgress({
|
|
14093
|
+
type: "text",
|
|
14094
|
+
text: event.text
|
|
14095
|
+
});
|
|
14096
|
+
else if (event.type === "thinking") onProgress({
|
|
14097
|
+
type: "thinking",
|
|
14098
|
+
text: event.text
|
|
14099
|
+
});
|
|
14100
|
+
else if (event.type === "status") onProgress({
|
|
14101
|
+
type: "status",
|
|
14102
|
+
text: event.text
|
|
14103
|
+
});
|
|
14104
|
+
else if (event.type === "tool") onProgress(event);
|
|
14105
|
+
else if (event.type === "done") finished = true;
|
|
14106
|
+
else throw new Error(event.message);
|
|
14107
|
+
};
|
|
14108
|
+
while (true) {
|
|
14109
|
+
const chunk = await reader.read();
|
|
14110
|
+
buffer += decoder.decode(chunk.value, { stream: !chunk.done });
|
|
14111
|
+
const lines = buffer.split("\n");
|
|
14112
|
+
buffer = lines.pop() ?? "";
|
|
14113
|
+
for (const line of lines) handle(line);
|
|
14114
|
+
if (chunk.done) break;
|
|
14115
|
+
}
|
|
14116
|
+
handle(buffer);
|
|
14117
|
+
if (!finished) throw new Error("The answer ended unexpectedly.");
|
|
14118
|
+
} finally {
|
|
14119
|
+
signal?.removeEventListener("abort", stop);
|
|
14120
|
+
carrier.abort();
|
|
13569
14121
|
}
|
|
13570
|
-
handle(buffer);
|
|
13571
|
-
if (!finished) throw new Error("The answer ended unexpectedly.");
|
|
13572
14122
|
}
|
|
13573
14123
|
//#endregion
|
|
13574
14124
|
//#region src/client/ClaudeSelectionAsk.tsx
|
|
@@ -14124,13 +14674,12 @@ window.__ModuleLoader__.load({
|
|
|
14124
14674
|
});
|
|
14125
14675
|
return stop;
|
|
14126
14676
|
}
|
|
14677
|
+
/** A beacon rather than a request: a finding is dropped when the connection
|
|
14678
|
+
* budget is spent, because the channel that reports the plugin's own failures
|
|
14679
|
+
* must never be the traffic that causes them — and must never queue behind
|
|
14680
|
+
* them either. */
|
|
14127
14681
|
function postToHost(report) {
|
|
14128
|
-
|
|
14129
|
-
method: "POST",
|
|
14130
|
-
credentials: "same-origin",
|
|
14131
|
-
headers: { "content-type": "application/json" },
|
|
14132
|
-
body: JSON.stringify(report)
|
|
14133
|
-
}).catch(() => {});
|
|
14682
|
+
pluginBeacon(CLAUDE_CLIENT_DIAGNOSTICS_PATH, report);
|
|
14134
14683
|
}
|
|
14135
14684
|
/** Renderer-side findings reach the Host log through here.
|
|
14136
14685
|
*
|
|
@@ -14217,23 +14766,15 @@ window.__ModuleLoader__.load({
|
|
|
14217
14766
|
/** Drop one user message and everything after it: the rows are hidden from
|
|
14218
14767
|
* this session's transcript and Claude resumes before that turn. */
|
|
14219
14768
|
async function rewindSession(sessionId, seq) {
|
|
14220
|
-
|
|
14221
|
-
|
|
14222
|
-
credentials: "same-origin",
|
|
14223
|
-
signal: pluginRequestSignal(PLUGIN_READ_TIMEOUT_MS),
|
|
14224
|
-
headers: {
|
|
14225
|
-
"content-type": "application/json",
|
|
14226
|
-
accept: "application/json"
|
|
14227
|
-
},
|
|
14228
|
-
body: JSON.stringify({
|
|
14769
|
+
try {
|
|
14770
|
+
await pluginWrite(CLAUDE_REWIND_PATH, "fast", void 0, { json: {
|
|
14229
14771
|
sessionId,
|
|
14230
14772
|
seq
|
|
14231
|
-
})
|
|
14232
|
-
})
|
|
14233
|
-
|
|
14234
|
-
|
|
14235
|
-
|
|
14236
|
-
throw new Error(typeof body?.error === "string" ? body.error : `http-${result.status}`);
|
|
14773
|
+
} });
|
|
14774
|
+
} catch (error) {
|
|
14775
|
+
if (!(error instanceof PluginRequestError)) throw error;
|
|
14776
|
+
throw new Error(error.code ?? error.reason);
|
|
14777
|
+
}
|
|
14237
14778
|
}
|
|
14238
14779
|
//#endregion
|
|
14239
14780
|
//#region src/client/ClaudeRewind.tsx
|
|
@@ -15449,15 +15990,10 @@ window.__ModuleLoader__.load({
|
|
|
15449
15990
|
//#region src/client/editor-open-api.ts
|
|
15450
15991
|
/** Launch the session's project in a desktop editor on the host machine. */
|
|
15451
15992
|
async function openProjectInEditor(sessionId, editor) {
|
|
15452
|
-
|
|
15453
|
-
|
|
15454
|
-
|
|
15455
|
-
|
|
15456
|
-
headers: { accept: "application/json" }
|
|
15457
|
-
});
|
|
15458
|
-
if (result.ok) return;
|
|
15459
|
-
const body = await result.json().catch(() => void 0);
|
|
15460
|
-
throw new Error(typeof body?.message === "string" ? body.message : "The editor could not be launched.");
|
|
15993
|
+
await pluginWrite(CLAUDE_EDITOR_OPEN_PATH, "fast", void 0, { query: {
|
|
15994
|
+
sessionId,
|
|
15995
|
+
editor
|
|
15996
|
+
} });
|
|
15461
15997
|
}
|
|
15462
15998
|
//#endregion
|
|
15463
15999
|
//#region src/client/ClaudeSessionMenu.tsx
|
|
@@ -15729,7 +16265,7 @@ window.__ModuleLoader__.load({
|
|
|
15729
16265
|
document.head.appendChild(element);
|
|
15730
16266
|
}
|
|
15731
16267
|
function ClaudeAgentPresetLabel({ t, hostT, roster, sessionId, useSessions }) {
|
|
15732
|
-
const preset = useSessions((state) => state.byId[sessionId]
|
|
16268
|
+
const preset = useSessions((state) => sessionRowPreset(state.byId[sessionId]));
|
|
15733
16269
|
const rows = useSyncExternalStore(roster.subscribe, roster.getSnapshot, roster.getSnapshot);
|
|
15734
16270
|
const { load } = roster;
|
|
15735
16271
|
useEffect(() => {
|
|
@@ -16162,6 +16698,10 @@ window.__ModuleLoader__.load({
|
|
|
16162
16698
|
globalSettingsNewSession: "修改仅对新建 Claude 会话生效。",
|
|
16163
16699
|
globalSettingsError: "全局设置保存失败",
|
|
16164
16700
|
outputStyle: "Output Style",
|
|
16701
|
+
renderer: "AI 输出渲染器",
|
|
16702
|
+
rendererPlugin: "插件渲染器",
|
|
16703
|
+
rendererNative: "DSH 原生渲染器",
|
|
16704
|
+
rendererEffect: "插件渲染器沿用本插件自带的转录视图:交错的正文、成组的工具卡片与活动行。DSH 原生渲染器改由 DSH 自身绘制:正文作为普通助手文本块,思考作为推理块,Claude 的顶层工具会镜像成原生工具卡片。修改从下一个回合起生效;已经产生的回合仍按记录时的渲染器显示,不会重绘。",
|
|
16165
16705
|
worktreeBranchPrefix: "Worktree 分支前缀",
|
|
16166
16706
|
worktreeBranchPrefixEffect: "分支前缀修改会应用于之后自动创建的 Worktree 分支;显式指定的完整分支名不会被修改。",
|
|
16167
16707
|
maxProcessesSetting: "Claude 进程上限",
|
|
@@ -16522,6 +17062,10 @@ window.__ModuleLoader__.load({
|
|
|
16522
17062
|
globalSettingsNewSession: "Changes apply only to new Claude sessions.",
|
|
16523
17063
|
globalSettingsError: "Global settings save failed",
|
|
16524
17064
|
outputStyle: "Output Style",
|
|
17065
|
+
renderer: "AI output renderer",
|
|
17066
|
+
rendererPlugin: "Plugin renderer",
|
|
17067
|
+
rendererNative: "DSH native renderer",
|
|
17068
|
+
rendererEffect: "The plugin renderer keeps this package’s own transcript: interleaved prose, grouped tool cards, and activity rows. The DSH native renderer hands the same turn to DSH itself — prose as ordinary assistant text blocks, thinking as reasoning blocks, and root Claude tools mirrored into native tool cards. A change applies from the next turn; turns already recorded keep the renderer they were recorded with and are not redrawn.",
|
|
16525
17069
|
worktreeBranchPrefix: "Worktree branch prefix",
|
|
16526
17070
|
worktreeBranchPrefixEffect: "Prefix changes apply to subsequently generated Worktree branches. Explicit full branch names remain unchanged.",
|
|
16527
17071
|
maxProcessesSetting: "Claude process limit",
|
|
@@ -16884,7 +17428,9 @@ window.__ModuleLoader__.load({
|
|
|
16884
17428
|
}), "dsh-claude: client copy");
|
|
16885
17429
|
const t = ctx.locale.bind(namespace);
|
|
16886
17430
|
ctx.effect(() => restyleHostChrome(), "dsh-claude: Host chrome restyling");
|
|
16887
|
-
const projections = new ClaudeProjectionStore()
|
|
17431
|
+
const projections = new ClaudeProjectionStore({ report: (kind, detail) => {
|
|
17432
|
+
diagnostics.report(kind, detail);
|
|
17433
|
+
} });
|
|
16888
17434
|
ctx.effect(() => ctx.inputTriggers.registerSource(createClaudeCommandSource(ctx, projections)), "dsh-claude: Claude slash source");
|
|
16889
17435
|
const sessions = ctx.get("sessions");
|
|
16890
17436
|
const workspaces = ctx.get("workspaces");
|