@norman-else/dsh-claude 0.1.20 → 0.1.21

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/lib/client.js CHANGED
@@ -2606,6 +2606,426 @@ window.__ModuleLoader__.load({
2606
2606
  });
2607
2607
  }
2608
2608
  //#endregion
2609
+ //#region src/client/projection.ts
2610
+ const EMPTY_CLAUDE_PROJECTION = {
2611
+ schemaVersion: 1,
2612
+ revision: 0,
2613
+ owned: false,
2614
+ commands: [],
2615
+ activities: []
2616
+ };
2617
+ const RETRY_DELAY_MS = 2e3;
2618
+ /** Coalesce stream deltas into at most one React notification per frame. */
2619
+ const FRAME_MS = 16;
2620
+ /** Typewriter smoothing: drain newly arrived prose over roughly this window,
2621
+ * so the CLI's paragraph-sized deltas read as a continuous character flow. */
2622
+ const REVEAL_WINDOW_MS = 1200;
2623
+ /** A burst larger than this (redaction rewrite, reconnect catch-up) shows
2624
+ * instantly instead of animating for a long stretch. */
2625
+ const MAX_INSTANT_REVEAL = 4e3;
2626
+ const MAX_ACTIVITIES = 1e4;
2627
+ const MAX_COMMANDS = 2e3;
2628
+ const MAX_REPOSITORY_TEXT_CHARS = 1024;
2629
+ const MAX_DIFF_CHARS = 262144;
2630
+ const MAX_REVIEW_COMMENTS = 50;
2631
+ const MAX_REVIEW_COMMENT_CHARS = 2e3;
2632
+ const MAX_TRANSCRIPT_CHARS = 64e3;
2633
+ function record$4(value) {
2634
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
2635
+ }
2636
+ function nonNegativeInteger(value) {
2637
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
2638
+ }
2639
+ function optionalBoundedString(value) {
2640
+ return value === void 0 || typeof value === "string" && value.length <= MAX_REPOSITORY_TEXT_CHARS;
2641
+ }
2642
+ function validateRepository(value) {
2643
+ const repository = record$4(value);
2644
+ if (repository === void 0 || ![
2645
+ "ready",
2646
+ "not-repository",
2647
+ "unavailable"
2648
+ ].includes(String(repository.status)) || typeof repository.cwd !== "string" || repository.cwd.length > MAX_REPOSITORY_TEXT_CHARS || !optionalBoundedString(repository.root) || !optionalBoundedString(repository.branch) || !optionalBoundedString(repository.remote) || repository.detached !== void 0 && typeof repository.detached !== "boolean" || repository.worktree !== void 0 && typeof repository.worktree !== "boolean" || repository.dirty !== void 0 && typeof repository.dirty !== "boolean" || repository.upstream !== void 0 && typeof repository.upstream !== "boolean" || repository.ahead !== void 0 && !nonNegativeInteger(repository.ahead) || repository.behind !== void 0 && !nonNegativeInteger(repository.behind)) return false;
2649
+ if (repository.diff !== void 0) {
2650
+ const diff = record$4(repository.diff);
2651
+ if (diff === void 0 || !nonNegativeInteger(diff.additions) || !nonNegativeInteger(diff.deletions) || !nonNegativeInteger(diff.files) || typeof diff.truncated !== "boolean" || diff.patch !== void 0 && (typeof diff.patch !== "string" || diff.patch.length > MAX_DIFF_CHARS)) return false;
2652
+ }
2653
+ if (repository.pullRequest === void 0) return true;
2654
+ const pullRequest = record$4(repository.pullRequest);
2655
+ if (pullRequest === void 0 || !Number.isSafeInteger(pullRequest.number) || Number(pullRequest.number) <= 0 || typeof pullRequest.title !== "string" || pullRequest.title.length > MAX_REPOSITORY_TEXT_CHARS || typeof pullRequest.url !== "string" || pullRequest.url.length > MAX_REPOSITORY_TEXT_CHARS || ![
2656
+ "open",
2657
+ "closed",
2658
+ "merged"
2659
+ ].includes(String(pullRequest.state)) || typeof pullRequest.draft !== "boolean" || ![
2660
+ "approved",
2661
+ "changes-requested",
2662
+ "review-required",
2663
+ "none"
2664
+ ].includes(String(pullRequest.review)) || ![
2665
+ "passing",
2666
+ "pending",
2667
+ "failing",
2668
+ "none"
2669
+ ].includes(String(pullRequest.checks)) || !optionalBoundedString(pullRequest.mergeState) || !optionalBoundedString(pullRequest.author) || !optionalBoundedString(pullRequest.baseBranch) || pullRequest.createdAt !== void 0 && (typeof pullRequest.createdAt !== "string" || !Number.isFinite(Date.parse(pullRequest.createdAt))) || pullRequest.mergedAt !== void 0 && (typeof pullRequest.mergedAt !== "string" || !Number.isFinite(Date.parse(pullRequest.mergedAt)))) return false;
2670
+ try {
2671
+ const url = new URL(pullRequest.url);
2672
+ return url.protocol === "https:" && url.hostname === "github.com";
2673
+ } catch {
2674
+ return false;
2675
+ }
2676
+ }
2677
+ /** Validate the public route envelope before publishing it to UI components. */
2678
+ function parseClaudeClientProjection(value) {
2679
+ const input = record$4(value);
2680
+ if (input === void 0 || input.schemaVersion !== 1 || !nonNegativeInteger(input.revision) || typeof input.owned !== "boolean" || !Array.isArray(input.commands) || input.commands.length > MAX_COMMANDS || !Array.isArray(input.activities) || input.activities.length > MAX_ACTIVITIES) throw new Error("invalid Claude sidecar projection");
2681
+ for (const item of input.commands) {
2682
+ const command = record$4(item);
2683
+ if (command === void 0 || typeof command.publicName !== "string" || typeof command.claudeName !== "string" || typeof command.description !== "string" || command.hint !== void 0 && typeof command.hint !== "string" || typeof command.prefixed !== "boolean") throw new Error("invalid Claude command projection");
2684
+ }
2685
+ for (const item of input.activities) {
2686
+ const activity = record$4(item);
2687
+ if (activity === void 0 || !nonNegativeInteger(activity.turn) || !nonNegativeInteger(activity.step) || !nonNegativeInteger(activity.ordinal) || typeof activity.kind !== "string") throw new Error("invalid Claude sidecar activity");
2688
+ }
2689
+ if (input.contextUsage !== void 0 && record$4(input.contextUsage) === void 0) throw new Error("invalid Claude context projection");
2690
+ const tasks = input.tasks === void 0 ? void 0 : record$4(input.tasks);
2691
+ if (tasks !== void 0 && !Array.isArray(tasks.tasks)) throw new Error("invalid Claude tasks projection");
2692
+ if (input.repository !== void 0 && !validateRepository(input.repository)) throw new Error("invalid Claude repository projection");
2693
+ if (input.reviewComments !== void 0) {
2694
+ if (!Array.isArray(input.reviewComments) || input.reviewComments.length > MAX_REVIEW_COMMENTS) throw new Error("invalid Claude review comment projection");
2695
+ for (const item of input.reviewComments) {
2696
+ const comment = record$4(item);
2697
+ if (comment === void 0 || typeof comment.id !== "string" || comment.id.length === 0 || comment.id.length > 128 || typeof comment.path !== "string" || comment.path.length === 0 || comment.path.length > MAX_REPOSITORY_TEXT_CHARS || !nonNegativeInteger(comment.line) || comment.side !== "old" && comment.side !== "new" || typeof comment.text !== "string" || comment.text.length > MAX_REVIEW_COMMENT_CHARS) throw new Error("invalid Claude review comment projection");
2698
+ }
2699
+ }
2700
+ return input;
2701
+ }
2702
+ /** Validate one incremental delta payload with the same rules as a snapshot. */
2703
+ function validateEnvelopeFragment(fragment) {
2704
+ parseClaudeClientProjection({
2705
+ schemaVersion: 1,
2706
+ revision: 0,
2707
+ owned: false,
2708
+ commands: [],
2709
+ activities: [],
2710
+ ...fragment
2711
+ });
2712
+ }
2713
+ function stepKeyOf(turn, step) {
2714
+ return `${turn}:${step}`;
2715
+ }
2716
+ const EMPTY_ACTIVITIES = [];
2717
+ /** Identity-stable per-step slice; untouched steps never re-render while
2718
+ * another step streams. Falls back to filtering for snapshot-only hooks. */
2719
+ function selectStepActivities(value, turn, step) {
2720
+ const sliced = value.byStep?.get(stepKeyOf(turn, step));
2721
+ if (sliced !== void 0) return sliced;
2722
+ const filtered = value.activities.filter((activity) => activity.turn === turn && activity.step === step);
2723
+ return filtered.length === 0 ? EMPTY_ACTIVITIES : filtered;
2724
+ }
2725
+ function isAbort(error) {
2726
+ return error?.name === "AbortError";
2727
+ }
2728
+ function delay(ms) {
2729
+ return new Promise((resolve) => {
2730
+ setTimeout(resolve, ms).unref?.();
2731
+ });
2732
+ }
2733
+ /** Create one lazy source: active subscribers open the Host NDJSON stream, and
2734
+ * a dropped stream reconnects with a fresh snapshot after a bounded delay. */
2735
+ function createClaudeProjectionSource(sessionId, fetchProjection = fetch, retryDelayMs = RETRY_DELAY_MS) {
2736
+ let snapshot = EMPTY_CLAUDE_PROJECTION;
2737
+ let revision = 0;
2738
+ let owned = false;
2739
+ let commands = [];
2740
+ let contextUsage;
2741
+ let tasks;
2742
+ let repository;
2743
+ let reviewComments;
2744
+ const byStep = /* @__PURE__ */ new Map();
2745
+ const stepOrder = [];
2746
+ /** Streaming prose still being revealed: full arrived text plus shown chars. */
2747
+ const reveal = /* @__PURE__ */ new Map();
2748
+ let disposed = false;
2749
+ let running = false;
2750
+ let controller;
2751
+ let frame;
2752
+ let usedAnimationFrame = false;
2753
+ const listeners = /* @__PURE__ */ new Set();
2754
+ const ensureStep = (turn, step) => {
2755
+ const key = stepKeyOf(turn, step);
2756
+ const existing = byStep.get(key);
2757
+ if (existing !== void 0) return existing;
2758
+ const created = [];
2759
+ byStep.set(key, created);
2760
+ const at = stepOrder.findIndex((entry) => entry.turn > turn || entry.turn === turn && entry.step > step);
2761
+ stepOrder.splice(at === -1 ? stepOrder.length : at, 0, {
2762
+ turn,
2763
+ step,
2764
+ key
2765
+ });
2766
+ return created;
2767
+ };
2768
+ const upsertActivity = (activity) => {
2769
+ const key = stepKeyOf(activity.turn, activity.step);
2770
+ const next = ensureStep(activity.turn, activity.step).slice();
2771
+ const index = next.findIndex((item) => item.ordinal === activity.ordinal);
2772
+ if (index === -1) {
2773
+ const at = next.findIndex((item) => item.ordinal > activity.ordinal);
2774
+ next.splice(at === -1 ? next.length : at, 0, activity);
2775
+ } else next[index] = activity;
2776
+ byStep.set(key, next);
2777
+ };
2778
+ const reset = (activities) => {
2779
+ byStep.clear();
2780
+ stepOrder.length = 0;
2781
+ reveal.clear();
2782
+ for (const activity of activities) ensureStep(activity.turn, activity.step).push(activity);
2783
+ };
2784
+ /** Advance every pending typewriter reveal by one frame's worth of characters.
2785
+ * The per-frame step scales with the backlog so any burst drains in roughly
2786
+ * REVEAL_WINDOW_MS. Returns whether another frame is still needed. */
2787
+ const advanceReveals = () => {
2788
+ let remaining = false;
2789
+ for (const [key, entry] of reveal) {
2790
+ const total = entry.full.text?.length ?? 0;
2791
+ if (entry.shown >= total) {
2792
+ reveal.delete(key);
2793
+ continue;
2794
+ }
2795
+ const step = Math.max(1, Math.ceil((total - entry.shown) * FRAME_MS / REVEAL_WINDOW_MS));
2796
+ entry.shown = Math.min(total, entry.shown + step);
2797
+ upsertActivity({
2798
+ ...entry.full,
2799
+ text: (entry.full.text ?? "").slice(0, entry.shown)
2800
+ });
2801
+ if (entry.shown >= total) reveal.delete(key);
2802
+ else remaining = true;
2803
+ }
2804
+ return remaining;
2805
+ };
2806
+ const publish = () => {
2807
+ frame = void 0;
2808
+ if (disposed) return;
2809
+ const revealing = advanceReveals();
2810
+ const activities = [];
2811
+ for (const entry of stepOrder) {
2812
+ const slice = byStep.get(entry.key);
2813
+ if (slice !== void 0) for (const item of slice) activities.push(item);
2814
+ }
2815
+ snapshot = {
2816
+ schemaVersion: 1,
2817
+ revision,
2818
+ owned,
2819
+ commands,
2820
+ activities,
2821
+ ...contextUsage === void 0 ? {} : { contextUsage },
2822
+ ...tasks === void 0 ? {} : { tasks },
2823
+ ...repository === void 0 ? {} : { repository },
2824
+ ...reviewComments === void 0 ? {} : { reviewComments },
2825
+ byStep: new Map(byStep)
2826
+ };
2827
+ for (const listener of [...listeners]) listener();
2828
+ if (revealing) schedulePublish();
2829
+ };
2830
+ const cancelFrame = () => {
2831
+ if (frame === void 0) return;
2832
+ if (usedAnimationFrame) globalThis.cancelAnimationFrame?.(frame);
2833
+ else clearTimeout(frame);
2834
+ frame = void 0;
2835
+ };
2836
+ const schedulePublish = () => {
2837
+ if (disposed || frame !== void 0 || listeners.size === 0) return;
2838
+ const raf = globalThis.requestAnimationFrame;
2839
+ if (typeof raf === "function") {
2840
+ usedAnimationFrame = true;
2841
+ frame = raf(publish);
2842
+ } else {
2843
+ usedAnimationFrame = false;
2844
+ frame = setTimeout(publish, FRAME_MS);
2845
+ }
2846
+ };
2847
+ const applyText = (event) => {
2848
+ const { turn, step, ordinal, append, text } = event;
2849
+ if (!nonNegativeInteger(turn) || !nonNegativeInteger(step) || !nonNegativeInteger(ordinal)) return false;
2850
+ if (append !== void 0 && (typeof append !== "string" || append.length > MAX_TRANSCRIPT_CHARS)) return false;
2851
+ if (text !== void 0 && (typeof text !== "string" || text.length > MAX_TRANSCRIPT_CHARS)) return false;
2852
+ if (append === void 0 && text === void 0) return false;
2853
+ const revealKey = `${turn}:${step}:${ordinal}`;
2854
+ const existing = byStep.get(stepKeyOf(turn, step))?.find((item) => item.ordinal === ordinal);
2855
+ const pending = reveal.get(revealKey);
2856
+ if (existing === void 0 && pending === void 0 && typeof text !== "string") return false;
2857
+ const baseText = pending?.full.text ?? existing?.text ?? "";
2858
+ const fullText = (typeof text === "string" ? text : `${baseText}${append}`).slice(0, MAX_TRANSCRIPT_CHARS);
2859
+ const full = {
2860
+ ...pending?.full ?? existing ?? {
2861
+ turn,
2862
+ step,
2863
+ ordinal,
2864
+ kind: "text",
2865
+ phase: "updated"
2866
+ },
2867
+ text: fullText
2868
+ };
2869
+ const shown = Math.min(pending?.shown ?? existing?.text?.length ?? 0, fullText.length);
2870
+ if (fullText.length - shown > MAX_INSTANT_REVEAL) {
2871
+ reveal.delete(revealKey);
2872
+ upsertActivity(full);
2873
+ return true;
2874
+ }
2875
+ reveal.set(revealKey, {
2876
+ full,
2877
+ shown
2878
+ });
2879
+ upsertActivity({
2880
+ ...full,
2881
+ text: fullText.slice(0, shown)
2882
+ });
2883
+ return true;
2884
+ };
2885
+ const applyLine = (line) => {
2886
+ const trimmed = line.trim();
2887
+ if (trimmed.length === 0) return;
2888
+ let value;
2889
+ try {
2890
+ value = JSON.parse(trimmed);
2891
+ } catch {
2892
+ return;
2893
+ }
2894
+ const event = record$4(value);
2895
+ if (event === void 0 || typeof event.type !== "string") return;
2896
+ try {
2897
+ switch (event.type) {
2898
+ case "snapshot": {
2899
+ const next = parseClaudeClientProjection(event);
2900
+ revision = next.revision;
2901
+ owned = next.owned;
2902
+ commands = next.commands;
2903
+ contextUsage = next.contextUsage;
2904
+ tasks = next.tasks;
2905
+ repository = next.repository;
2906
+ reviewComments = next.reviewComments;
2907
+ reset(next.activities);
2908
+ break;
2909
+ }
2910
+ case "text":
2911
+ if (!applyText(event)) return;
2912
+ revision += 1;
2913
+ break;
2914
+ case "activity":
2915
+ validateEnvelopeFragment({ activities: [event.activity] });
2916
+ upsertActivity(event.activity);
2917
+ revision += 1;
2918
+ break;
2919
+ case "contextUsage":
2920
+ validateEnvelopeFragment({ contextUsage: event.value });
2921
+ contextUsage = event.value;
2922
+ revision += 1;
2923
+ break;
2924
+ case "tasks":
2925
+ validateEnvelopeFragment({ tasks: event.value });
2926
+ tasks = event.value;
2927
+ revision += 1;
2928
+ break;
2929
+ case "meta":
2930
+ validateEnvelopeFragment({
2931
+ owned: event.owned,
2932
+ commands: event.commands,
2933
+ ...event.repository === void 0 ? {} : { repository: event.repository },
2934
+ ...event.reviewComments === void 0 ? {} : { reviewComments: event.reviewComments }
2935
+ });
2936
+ owned = event.owned;
2937
+ commands = event.commands;
2938
+ repository = event.repository;
2939
+ reviewComments = event.reviewComments;
2940
+ revision += 1;
2941
+ break;
2942
+ default: return;
2943
+ }
2944
+ } catch {
2945
+ return;
2946
+ }
2947
+ schedulePublish();
2948
+ };
2949
+ const run = async () => {
2950
+ if (running) return;
2951
+ running = true;
2952
+ try {
2953
+ while (!disposed && listeners.size > 0) {
2954
+ controller = new AbortController();
2955
+ try {
2956
+ const response = await fetchProjection(`${CLAUDE_PROJECTION_PATH}/${encodeURIComponent(sessionId)}/stream`, {
2957
+ headers: { accept: "application/x-ndjson" },
2958
+ signal: controller.signal
2959
+ });
2960
+ if (!response.ok) throw new Error(`Claude projection stream failed (${response.status})`);
2961
+ if (response.body === null) throw new Error("Claude projection stream is unavailable");
2962
+ const reader = response.body.getReader();
2963
+ const decoder = new TextDecoder();
2964
+ let buffer = "";
2965
+ while (true) {
2966
+ if (disposed || listeners.size === 0) {
2967
+ await reader.cancel().catch(() => void 0);
2968
+ break;
2969
+ }
2970
+ const chunk = await reader.read();
2971
+ buffer += decoder.decode(chunk.value, { stream: !chunk.done });
2972
+ const lines = buffer.split("\n");
2973
+ buffer = lines.pop() ?? "";
2974
+ for (const line of lines) applyLine(line);
2975
+ if (chunk.done) break;
2976
+ }
2977
+ } catch (error) {
2978
+ if (isAbort(error)) return;
2979
+ } finally {
2980
+ controller = void 0;
2981
+ }
2982
+ if (disposed || listeners.size === 0) return;
2983
+ await delay(retryDelayMs);
2984
+ }
2985
+ } finally {
2986
+ running = false;
2987
+ }
2988
+ };
2989
+ return {
2990
+ getSnapshot: () => snapshot,
2991
+ subscribe(listener) {
2992
+ if (disposed) return () => {};
2993
+ const wasIdle = listeners.size === 0;
2994
+ listeners.add(listener);
2995
+ if (wasIdle) run();
2996
+ return () => {
2997
+ listeners.delete(listener);
2998
+ if (listeners.size !== 0) return;
2999
+ controller?.abort();
3000
+ controller = void 0;
3001
+ cancelFrame();
3002
+ };
3003
+ },
3004
+ dispose() {
3005
+ disposed = true;
3006
+ listeners.clear();
3007
+ controller?.abort();
3008
+ controller = void 0;
3009
+ cancelFrame();
3010
+ }
3011
+ };
3012
+ }
3013
+ var ClaudeProjectionStore = class {
3014
+ #sources = /* @__PURE__ */ new Map();
3015
+ source(sessionId) {
3016
+ let source = this.#sources.get(sessionId);
3017
+ if (source === void 0) {
3018
+ source = createClaudeProjectionSource(sessionId);
3019
+ this.#sources.set(sessionId, source);
3020
+ }
3021
+ return source;
3022
+ }
3023
+ dispose() {
3024
+ for (const source of this.#sources.values()) source.dispose();
3025
+ this.#sources.clear();
3026
+ }
3027
+ };
3028
+ //#endregion
2609
3029
  //#region src/client/ClaudeActivityNode.tsx
2610
3030
  const EMPTY_TASKS = [];
2611
3031
  const ACTIVITY_CSS = [
@@ -2730,7 +3150,7 @@ window.__ModuleLoader__.load({
2730
3150
  return value;
2731
3151
  }
2732
3152
  }
2733
- function record$4(value) {
3153
+ function record$3(value) {
2734
3154
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
2735
3155
  }
2736
3156
  function text(value) {
@@ -2744,7 +3164,7 @@ window.__ModuleLoader__.load({
2744
3164
  if (typeof value === "string") return value;
2745
3165
  if (typeof value === "number" || typeof value === "boolean") return String(value);
2746
3166
  if (Array.isArray(value)) return value.map(displayValue).join(", ");
2747
- return record$4(value) === void 0 ? String(value) : Object.entries(value).map(([key, item]) => `${key}: ${displayValue(item)}`).join("\n");
3167
+ return record$3(value) === void 0 ? String(value) : Object.entries(value).map(([key, item]) => `${key}: ${displayValue(item)}`).join("\n");
2748
3168
  }
2749
3169
  function Section({ title, children }) {
2750
3170
  return /* @__PURE__ */ jsxs("section", {
@@ -2814,15 +3234,15 @@ window.__ModuleLoader__.load({
2814
3234
  function ToolPresentation({ tool, t }) {
2815
3235
  const inputValue = parsedValue(tool.input);
2816
3236
  const outputValue = parsedValue(tool.output);
2817
- const input = record$4(inputValue);
2818
- const output = record$4(outputValue);
3237
+ const input = record$3(inputValue);
3238
+ const output = record$3(outputValue);
2819
3239
  const outputTitle = tool.isError === true ? t("toolError") : t("toolOutput");
2820
3240
  if (tool.diffs !== void 0) return /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(DiffBlock, { diffs: [...tool.diffs] }), /* @__PURE__ */ jsx(TextDetail, {
2821
3241
  title: outputTitle,
2822
3242
  value: typeof outputValue === "string" ? outputValue : void 0
2823
3243
  })] });
2824
3244
  if (tool.toolName === "Read") {
2825
- const file = record$4(output?.file);
3245
+ const file = record$3(output?.file);
2826
3246
  const path = text(file?.filePath) ?? text(input?.file_path);
2827
3247
  const content = text(file?.content) ?? (typeof outputValue === "string" ? outputValue : void 0);
2828
3248
  const offset = numberValue(input?.offset) ?? 1;
@@ -2984,7 +3404,7 @@ window.__ModuleLoader__.load({
2984
3404
  function ClaudeActivityNode({ node, useClaudeProjection, t }) {
2985
3405
  ensureCss();
2986
3406
  const marker = node.data;
2987
- const activities = useClaudeProjection((value) => value.activities);
3407
+ const activities = useClaudeProjection((value) => selectStepActivities(value, marker.turn, marker.step));
2988
3408
  const tasks = useClaudeProjection((value) => value.tasks?.tasks ?? EMPTY_TASKS);
2989
3409
  const items = useMemo(() => transcriptItemsForStep(activities, marker.turn, marker.step, tasks), [
2990
3410
  activities,
@@ -3851,7 +4271,7 @@ window.__ModuleLoader__.load({
3851
4271
  }
3852
4272
  //#endregion
3853
4273
  //#region src/client/review-comment-api.ts
3854
- function record$3(value) {
4274
+ function record$2(value) {
3855
4275
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
3856
4276
  }
3857
4277
  async function post(path, sessionId, body) {
@@ -3866,13 +4286,13 @@ window.__ModuleLoader__.load({
3866
4286
  });
3867
4287
  const value = await response.json();
3868
4288
  if (!response.ok) {
3869
- const error = record$3(value);
4289
+ const error = record$2(value);
3870
4290
  throw new Error(typeof error?.message === "string" ? error.message : "Review comment request failed.");
3871
4291
  }
3872
4292
  return value;
3873
4293
  }
3874
4294
  async function addReviewComment(sessionId, comment) {
3875
- const created = record$3(record$3(await post("", sessionId, comment))?.comment);
4295
+ const created = record$2(record$2(await post("", sessionId, comment))?.comment);
3876
4296
  if (created === void 0 || typeof created.id !== "string") throw new Error("Invalid review comment response.");
3877
4297
  return created;
3878
4298
  }
@@ -4037,33 +4457,33 @@ window.__ModuleLoader__.load({
4037
4457
  if (commit !== void 0) this.commit = commit;
4038
4458
  }
4039
4459
  };
4040
- function record$2(value) {
4460
+ function record$1(value) {
4041
4461
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
4042
4462
  }
4043
4463
  async function response$1(pending) {
4044
4464
  const result = await pending;
4045
4465
  const body = await result.json();
4046
4466
  if (!result.ok) {
4047
- const error = record$2(body);
4467
+ const error = record$1(body);
4048
4468
  throw new RepositoryActionClientError(typeof error?.message === "string" ? error.message : "Repository action failed.", typeof error?.error === "string" ? error.error : void 0, typeof error?.commit === "string" ? error.commit : void 0);
4049
4469
  }
4050
4470
  return body;
4051
4471
  }
4052
4472
  function preview(value) {
4053
- const input = record$2(value);
4473
+ const input = record$1(value);
4054
4474
  if (input === void 0 || typeof input.root !== "string" || typeof input.branch !== "string" || typeof input.head !== "string" || typeof input.fingerprint !== "string" || !Array.isArray(input.files) || typeof input.patch !== "string" || typeof input.truncated !== "boolean" || typeof input.hasStaged !== "boolean" || typeof input.hasUnstaged !== "boolean" || typeof input.hasUntracked !== "boolean" || input.upstream !== void 0 && typeof input.upstream !== "string" || !Array.isArray(input.unpushedCommits) || typeof input.unpushedTruncated !== "boolean") throw new Error("Invalid repository action preview.");
4055
4475
  for (const file of input.files) {
4056
- const item = record$2(file);
4476
+ const item = record$1(file);
4057
4477
  if (item === void 0 || typeof item.path !== "string" || typeof item.staged !== "boolean" || typeof item.unstaged !== "boolean" || typeof item.untracked !== "boolean") throw new Error("Invalid repository action file.");
4058
4478
  }
4059
4479
  for (const commit of input.unpushedCommits) {
4060
- const item = record$2(commit);
4480
+ const item = record$1(commit);
4061
4481
  if (item === void 0 || typeof item.hash !== "string" || typeof item.subject !== "string") throw new Error("Invalid repository action commit.");
4062
4482
  }
4063
4483
  return input;
4064
4484
  }
4065
4485
  function result(value) {
4066
- const input = record$2(value);
4486
+ const input = record$1(value);
4067
4487
  if (input === void 0 || typeof input.commit !== "string" || typeof input.pushed !== "boolean" || input.pullRequestUrl !== void 0 && typeof input.pullRequestUrl !== "string") throw new Error("Invalid repository action result.");
4068
4488
  return input;
4069
4489
  }
@@ -4079,7 +4499,7 @@ window.__ModuleLoader__.load({
4079
4499
  })));
4080
4500
  }
4081
4501
  async function generateCommitMessage(sessionId, fingerprint, signal) {
4082
- const value = record$2(await response$1(fetch(endpoint("/message", sessionId), {
4502
+ const value = record$1(await response$1(fetch(endpoint("/message", sessionId), {
4083
4503
  method: "POST",
4084
4504
  credentials: "same-origin",
4085
4505
  headers: {
@@ -5019,16 +5439,16 @@ window.__ModuleLoader__.load({
5019
5439
  "saving-worktree",
5020
5440
  "switching-branch"
5021
5441
  ]);
5022
- function record$1(value) {
5442
+ function record(value) {
5023
5443
  return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
5024
5444
  }
5025
5445
  function setupResult(value) {
5026
- const item = record$1(value);
5446
+ const item = record(value);
5027
5447
  if (item === void 0 || item.mode !== "checkout" && item.mode !== "worktree" || typeof item.root !== "string" || typeof item.path !== "string" || typeof item.branch !== "string" || item.leaseId !== void 0 && typeof item.leaseId !== "string") return void 0;
5028
5448
  return item;
5029
5449
  }
5030
5450
  function parseRepositorySetupEvent(line, onProgress) {
5031
- const event = record$1(JSON.parse(line));
5451
+ const event = record(JSON.parse(line));
5032
5452
  if (event?.type === "progress" && typeof event.stage === "string" && HOST_STAGES.has(event.stage)) {
5033
5453
  onProgress(event.stage);
5034
5454
  return;
@@ -5572,170 +5992,6 @@ window.__ModuleLoader__.load({
5572
5992
  }), portal);
5573
5993
  }
5574
5994
  //#endregion
5575
- //#region src/client/projection.ts
5576
- const EMPTY_CLAUDE_PROJECTION = {
5577
- schemaVersion: 1,
5578
- revision: 0,
5579
- owned: false,
5580
- commands: [],
5581
- activities: []
5582
- };
5583
- const POLL_INTERVAL_MS = 2e3;
5584
- const MAX_ACTIVITIES = 1e4;
5585
- const MAX_COMMANDS = 2e3;
5586
- const MAX_REPOSITORY_TEXT_CHARS = 1024;
5587
- const MAX_DIFF_CHARS = 262144;
5588
- const MAX_REVIEW_COMMENTS = 50;
5589
- const MAX_REVIEW_COMMENT_CHARS = 2e3;
5590
- function record(value) {
5591
- return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
5592
- }
5593
- function nonNegativeInteger(value) {
5594
- return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
5595
- }
5596
- function optionalBoundedString(value) {
5597
- return value === void 0 || typeof value === "string" && value.length <= MAX_REPOSITORY_TEXT_CHARS;
5598
- }
5599
- function validateRepository(value) {
5600
- const repository = record(value);
5601
- if (repository === void 0 || ![
5602
- "ready",
5603
- "not-repository",
5604
- "unavailable"
5605
- ].includes(String(repository.status)) || typeof repository.cwd !== "string" || repository.cwd.length > MAX_REPOSITORY_TEXT_CHARS || !optionalBoundedString(repository.root) || !optionalBoundedString(repository.branch) || !optionalBoundedString(repository.remote) || repository.detached !== void 0 && typeof repository.detached !== "boolean" || repository.worktree !== void 0 && typeof repository.worktree !== "boolean" || repository.dirty !== void 0 && typeof repository.dirty !== "boolean" || repository.upstream !== void 0 && typeof repository.upstream !== "boolean" || repository.ahead !== void 0 && !nonNegativeInteger(repository.ahead) || repository.behind !== void 0 && !nonNegativeInteger(repository.behind)) return false;
5606
- if (repository.diff !== void 0) {
5607
- const diff = record(repository.diff);
5608
- if (diff === void 0 || !nonNegativeInteger(diff.additions) || !nonNegativeInteger(diff.deletions) || !nonNegativeInteger(diff.files) || typeof diff.truncated !== "boolean" || diff.patch !== void 0 && (typeof diff.patch !== "string" || diff.patch.length > MAX_DIFF_CHARS)) return false;
5609
- }
5610
- if (repository.pullRequest === void 0) return true;
5611
- const pullRequest = record(repository.pullRequest);
5612
- if (pullRequest === void 0 || !Number.isSafeInteger(pullRequest.number) || Number(pullRequest.number) <= 0 || typeof pullRequest.title !== "string" || pullRequest.title.length > MAX_REPOSITORY_TEXT_CHARS || typeof pullRequest.url !== "string" || pullRequest.url.length > MAX_REPOSITORY_TEXT_CHARS || ![
5613
- "open",
5614
- "closed",
5615
- "merged"
5616
- ].includes(String(pullRequest.state)) || typeof pullRequest.draft !== "boolean" || ![
5617
- "approved",
5618
- "changes-requested",
5619
- "review-required",
5620
- "none"
5621
- ].includes(String(pullRequest.review)) || ![
5622
- "passing",
5623
- "pending",
5624
- "failing",
5625
- "none"
5626
- ].includes(String(pullRequest.checks)) || !optionalBoundedString(pullRequest.mergeState) || !optionalBoundedString(pullRequest.author) || !optionalBoundedString(pullRequest.baseBranch) || pullRequest.createdAt !== void 0 && (typeof pullRequest.createdAt !== "string" || !Number.isFinite(Date.parse(pullRequest.createdAt))) || pullRequest.mergedAt !== void 0 && (typeof pullRequest.mergedAt !== "string" || !Number.isFinite(Date.parse(pullRequest.mergedAt)))) return false;
5627
- try {
5628
- const url = new URL(pullRequest.url);
5629
- return url.protocol === "https:" && url.hostname === "github.com";
5630
- } catch {
5631
- return false;
5632
- }
5633
- }
5634
- /** Validate the public route envelope before publishing it to UI components. */
5635
- function parseClaudeClientProjection(value) {
5636
- const input = record(value);
5637
- if (input === void 0 || input.schemaVersion !== 1 || !nonNegativeInteger(input.revision) || typeof input.owned !== "boolean" || !Array.isArray(input.commands) || input.commands.length > MAX_COMMANDS || !Array.isArray(input.activities) || input.activities.length > MAX_ACTIVITIES) throw new Error("invalid Claude sidecar projection");
5638
- for (const item of input.commands) {
5639
- const command = record(item);
5640
- if (command === void 0 || typeof command.publicName !== "string" || typeof command.claudeName !== "string" || typeof command.description !== "string" || command.hint !== void 0 && typeof command.hint !== "string" || typeof command.prefixed !== "boolean") throw new Error("invalid Claude command projection");
5641
- }
5642
- for (const item of input.activities) {
5643
- const activity = record(item);
5644
- if (activity === void 0 || !nonNegativeInteger(activity.turn) || !nonNegativeInteger(activity.step) || !nonNegativeInteger(activity.ordinal) || typeof activity.kind !== "string") throw new Error("invalid Claude sidecar activity");
5645
- }
5646
- if (input.contextUsage !== void 0 && record(input.contextUsage) === void 0) throw new Error("invalid Claude context projection");
5647
- const tasks = input.tasks === void 0 ? void 0 : record(input.tasks);
5648
- if (tasks !== void 0 && !Array.isArray(tasks.tasks)) throw new Error("invalid Claude tasks projection");
5649
- if (input.repository !== void 0 && !validateRepository(input.repository)) throw new Error("invalid Claude repository projection");
5650
- if (input.reviewComments !== void 0) {
5651
- if (!Array.isArray(input.reviewComments) || input.reviewComments.length > MAX_REVIEW_COMMENTS) throw new Error("invalid Claude review comment projection");
5652
- for (const item of input.reviewComments) {
5653
- const comment = record(item);
5654
- if (comment === void 0 || typeof comment.id !== "string" || comment.id.length === 0 || comment.id.length > 128 || typeof comment.path !== "string" || comment.path.length === 0 || comment.path.length > MAX_REPOSITORY_TEXT_CHARS || !nonNegativeInteger(comment.line) || comment.side !== "old" && comment.side !== "new" || typeof comment.text !== "string" || comment.text.length > MAX_REVIEW_COMMENT_CHARS) throw new Error("invalid Claude review comment projection");
5655
- }
5656
- }
5657
- return input;
5658
- }
5659
- /** Create one lazy source: active subscribers trigger an immediate load and bounded polling. */
5660
- function createClaudeProjectionSource(sessionId, fetchProjection = fetch, pollIntervalMs = POLL_INTERVAL_MS) {
5661
- let snapshot = EMPTY_CLAUDE_PROJECTION;
5662
- let timer;
5663
- let controller;
5664
- let disposed = false;
5665
- const listeners = /* @__PURE__ */ new Set();
5666
- const schedule = () => {
5667
- if (disposed || listeners.size === 0) return;
5668
- timer = setTimeout(() => {
5669
- refresh();
5670
- }, pollIntervalMs);
5671
- };
5672
- const refresh = async () => {
5673
- if (disposed || listeners.size === 0) return;
5674
- controller?.abort();
5675
- controller = new AbortController();
5676
- try {
5677
- const response = await fetchProjection(`${CLAUDE_PROJECTION_PATH}/${encodeURIComponent(sessionId)}`, {
5678
- headers: { accept: "application/json" },
5679
- signal: controller.signal
5680
- });
5681
- if (!response.ok) throw new Error(`Claude projection request failed (${response.status})`);
5682
- const next = parseClaudeClientProjection(await response.json());
5683
- const commandCatalogChanged = JSON.stringify(next.commands) !== JSON.stringify(snapshot.commands);
5684
- const repositoryChanged = JSON.stringify(next.repository) !== JSON.stringify(snapshot.repository);
5685
- const reviewCommentsChanged = JSON.stringify(next.reviewComments) !== JSON.stringify(snapshot.reviewComments);
5686
- if (next.revision !== snapshot.revision || next.owned !== snapshot.owned || commandCatalogChanged || repositoryChanged || reviewCommentsChanged) {
5687
- snapshot = next;
5688
- for (const listener of [...listeners]) listener();
5689
- }
5690
- } catch (error) {
5691
- if (error instanceof DOMException && error.name === "AbortError") return;
5692
- } finally {
5693
- controller = void 0;
5694
- schedule();
5695
- }
5696
- };
5697
- return {
5698
- getSnapshot: () => snapshot,
5699
- subscribe(listener) {
5700
- if (disposed) return () => {};
5701
- const wasIdle = listeners.size === 0;
5702
- listeners.add(listener);
5703
- if (wasIdle) refresh();
5704
- return () => {
5705
- listeners.delete(listener);
5706
- if (listeners.size !== 0) return;
5707
- if (timer !== void 0) clearTimeout(timer);
5708
- timer = void 0;
5709
- controller?.abort();
5710
- controller = void 0;
5711
- };
5712
- },
5713
- dispose() {
5714
- disposed = true;
5715
- listeners.clear();
5716
- if (timer !== void 0) clearTimeout(timer);
5717
- timer = void 0;
5718
- controller?.abort();
5719
- controller = void 0;
5720
- }
5721
- };
5722
- }
5723
- var ClaudeProjectionStore = class {
5724
- #sources = /* @__PURE__ */ new Map();
5725
- source(sessionId) {
5726
- let source = this.#sources.get(sessionId);
5727
- if (source === void 0) {
5728
- source = createClaudeProjectionSource(sessionId);
5729
- this.#sources.set(sessionId, source);
5730
- }
5731
- return source;
5732
- }
5733
- dispose() {
5734
- for (const source of this.#sources.values()) source.dispose();
5735
- this.#sources.clear();
5736
- }
5737
- };
5738
- //#endregion
5739
5995
  //#region src/client/claude-command-source.ts
5740
5996
  const SOURCE_NAME = "Claude Code";
5741
5997
  function commandsFor(store, session) {