Repository navigation
feat(desktop): ChatV3Pane — flagged chat pane on the new spine #6207
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
12dbbbe
2d6d16f
ef3c57b
0d3bcd0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,69 @@ | ||
| import type { UserContent } from "@superset/chat/protocol"; | ||
| import { useCallback, useState } from "react"; | ||
| import type { HarnessId } from "./components/NewSessionView"; | ||
| import { NewSessionView } from "./components/NewSessionView"; | ||
| import { SessionPicker } from "./components/SessionPicker"; | ||
| import { SessionView } from "./components/SessionView"; | ||
| import { useSessionClient } from "./hooks/useSessionClient"; | ||
|
|
||
| export function ChatV3Pane({ | ||
| onSessionIdChange, | ||
| sessionId, | ||
| workspaceId, | ||
| }: { | ||
| workspaceId: string; | ||
| sessionId: string | null; | ||
| onSessionIdChange: (sessionId: string | null) => void; | ||
| }) { | ||
| const { client, wiring } = useSessionClient(sessionId); | ||
| const [harness, setHarness] = useState<HarnessId>("claude-code"); | ||
| const [pendingFirstPrompt, setPendingFirstPrompt] = useState< | ||
| UserContent[] | null | ||
| >(null); | ||
|
|
||
| const createSession = useCallback( | ||
| async (content: UserContent[] | null) => { | ||
| const created = await wiring.transport.createSession({ | ||
| commandId: crypto.randomUUID(), | ||
| workspaceId, | ||
| harness, | ||
| }); | ||
| setPendingFirstPrompt(content); | ||
| onSessionIdChange(created.sessionId); | ||
| }, | ||
| [wiring.transport, workspaceId, harness, onSessionIdChange], | ||
| ); | ||
|
|
||
| const picker = ( | ||
| <SessionPicker | ||
| activeSessionId={sessionId} | ||
| onNewSession={() => onSessionIdChange(null)} | ||
| onSelect={onSessionIdChange} | ||
|
Comment on lines
+39
to
+41
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win Bind
🤖 Prompt for AI Agents |
||
| transport={wiring.transport} | ||
| workspaceId={workspaceId} | ||
| /> | ||
| ); | ||
|
|
||
| if (!client || !sessionId) { | ||
| return ( | ||
| <NewSessionView | ||
| harness={harness} | ||
| headerLeft={picker} | ||
| onHarnessChange={setHarness} | ||
| onSend={(content) => void createSession(content)} | ||
| workspaceId={workspaceId} | ||
| /> | ||
| ); | ||
| } | ||
|
|
||
| return ( | ||
| <SessionView | ||
| client={client} | ||
| headerLeft={picker} | ||
| key={sessionId} | ||
| onFirstPromptSent={() => setPendingFirstPrompt(null)} | ||
| pendingFirstPrompt={pendingFirstPrompt} | ||
| sessionId={sessionId} | ||
| /> | ||
| ); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,109 @@ | ||
| import type { OutboxEntry } from "@superset/chat/core"; | ||
| import type { UserContent } from "@superset/chat/protocol"; | ||
| import { Button } from "@superset/ui/button"; | ||
| import { Textarea } from "@superset/ui/textarea"; | ||
| import { ArrowUp, Square } from "lucide-react"; | ||
| import { useEffect, useRef, useState } from "react"; | ||
|
|
||
| const DRAFT_DEBOUNCE_MS = 300; | ||
|
|
||
| export type ComposerProps = { | ||
| draftKey: string; | ||
| outbox: OutboxEntry[]; | ||
| onSend: (content: UserContent[]) => OutboxEntry | null; | ||
| placeholder?: string; | ||
| disabled?: boolean; | ||
| onCancelTurn?: (() => void) | null; | ||
| }; | ||
|
|
||
| export function Composer({ | ||
| disabled, | ||
| draftKey, | ||
| onCancelTurn, | ||
| onSend, | ||
| outbox, | ||
| placeholder, | ||
| }: ComposerProps) { | ||
| const [value, setValue] = useState( | ||
| () => window.localStorage.getItem(draftKey) ?? "", | ||
| ); | ||
| const valueRef = useRef(value); | ||
| valueRef.current = value; | ||
| const pendingRef = useRef<{ clientId: string; sentText: string } | null>( | ||
| null, | ||
| ); | ||
|
Comment on lines
+27
to
+34
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win Bound the per-session draft keys or move them to SQLite.
Add one of the following:
Also document the bound and deletion owner for the key family. Based on learnings from the coding guidelines: "Use localStorage only for small singleton UI state; entity-scoped unbounded data or payloads larger than a few KB belongs in SQLite via 🤖 Prompt for AI AgentsSource: Coding guidelines |
||
|
|
||
| useEffect(() => { | ||
| const timer = setTimeout(() => { | ||
| if (value === "") window.localStorage.removeItem(draftKey); | ||
| else window.localStorage.setItem(draftKey, value); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: Abandoned chat drafts can grow renderer localStorage without a size bound or cleanup path: each session/workspace gets a separate key and unsent drafts survive indefinitely. Add a bounded/expiring reconciliation strategy or move entity-scoped draft persistence to the supported SQLite layer. Prompt for AI agents
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: The desktop persisted-key registry check will fail for this new Prompt for AI agents |
||
| }, DRAFT_DEBOUNCE_MS); | ||
| return () => clearTimeout(timer); | ||
| }, [value, draftKey]); | ||
|
|
||
| useEffect(() => { | ||
| return () => { | ||
| const latest = valueRef.current; | ||
| if (latest === "") window.localStorage.removeItem(draftKey); | ||
| else window.localStorage.setItem(draftKey, latest); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P1: Switching sessions before a sent message is echoed can resurrect that message as a draft: the unmount cleanup saves the pending sent text, but the pending outbox/echo state is destroyed with the old session client. Make cleanup/send state durable across the session switch or remove/mark the draft when dispatching so an accepted prompt cannot be duplicated. Prompt for AI agents |
||
| }; | ||
| }, [draftKey]); | ||
|
|
||
| useEffect(() => { | ||
| const pending = pendingRef.current; | ||
| if (!pending) return; | ||
| if (outbox.some((entry) => entry.clientId === pending.clientId)) return; | ||
| pendingRef.current = null; | ||
| if (valueRef.current === pending.sentText) { | ||
| setValue(""); | ||
| window.localStorage.removeItem(draftKey); | ||
| } | ||
| }, [outbox, draftKey]); | ||
|
Comment on lines
+52
to
+61
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Description: Inspect OutboxEntryState members and discard/confirm transitions.
fd -t f 'outbox.ts' packages/chat/src | xargs rg -n -C3 'OutboxEntryState|state\s*[:=]\s*"|discard|confirm'Repository: superset-sh/superset Length of output: 1823 🏁 Script executed: #!/bin/bash
set -euo pipefail
FILE="apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/\$workspaceId/hooks/usePaneRegistry/components/ChatV3Pane/components/Composer/Composer.tsx"
echo "== file exists =="
test -f "$FILE" && echo yes || echo no
echo "== composer outline =="
ast-grep outline "$FILE" --view expanded | sed -n '1,220p'
echo "== composer relevant lines =="
cat -n "$FILE" | sed -n '1,140p'
echo "== references to clear/draft/pending text and outbox discard/confirm =="
rg -n -C3 'discards?|discardPrompt|onDiscard|confirm|pendingRef|sentText|setValue|localStorage.removeItem|outbox' apps/desktop/src/renderer packages -g '*.ts' -g '*.tsx' | sed -n '1,240p'Repository: superset-sh/superset Length of output: 27039 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "== locate ChatV3Pane files =="
fd -t f 'ChatV3Pane|Transcript|Outbox' apps/desktop/src/renderer packages -i | sed -n '1,200p'
echo "== ChatV3Pane references to discard/confirm/outbox =="
rg -n -C4 'outbox|confirm|discard|onConfirm|onDiscard|onDiscardPrompt' apps/desktop/src/renderer -g '*.ts' -g '*.tsx' | sed -n '1,260p'
echo "== focused app files =="
for name in "ChatV3Pane.tsx" "Transcript.tsx" "ChatV3Pane.test.tsx"; do
echo "-- $name --"
fd -t f "$name" apps/desktop/src/renderer -i | while read -r f; do
echo "FILE: $f"
rg -n -C4 'outbox|confirm|discard|onConfirm|onDiscard|onDiscardPrompt' "$f" || true
done
done
echo "== chat package transcript-related references =="
rg -n -C4 'outbox|confirm|discard|pendingRef|sentText|onDiscardPrompt|onConfirm' packages apps -g '*.ts' -g '*.tsx' -g '*.test-data.ts' || trueRepository: superset-sh/superset Length of output: 50377 🏁 Script executed: #!/bin/bash
set -euo pipefail
FILE="apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/\$workspaceId/hooks/usePaneRegistry/components/ChatV3Pane/components/Composer/Composer.tsx"
PANEDIR="apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workcase/\$workspaceId/hooks/usePaneRegistry/components/ChatV3Pane"
APPFILES="apps/desktop/src/renderer"
echo "== exact Composer file =="
wc -l "$FILE"
sed -n '1,130p' "$FILE"
echo "== exact ChatV3Pane file candidates =="
fd -i 'ChatV3Pane\.tsx$' "$APPFILES" | while read -r p; do
echo "---- $p ----"
rg -n -C5 'outbox|confirm|discard|onConfirm|onDiscard|onDiscardPrompt' "$p" || true
done
echo "== exact Transcript file candidates =="
fd -i 'Transcript\.tsx$' "$APPFILES" | while read -r p; do
echo "---- $p ----"
rg -n -C5 'outbox|confirm|discard|onConfirm|onDiscard|onDiscardPrompt' "$p" || true
done
echo "== focused app renderer search exact terms =="
rg -n -w -C3 '\'outbox\\'|\'confirm\\'|\'discard\\'|onConfirm|onDiscard|onDiscardPrompt|outbox' "$APPFILES" -g '*.ts' -g '*.tsx' | sed -n '1,260p'
echo "== focused package chat-outbox exact search =="
find packages -path '*chat*' -type f \( -name '*.ts' -o -name '*.tsx' \) -print0 | xargs -0 rg -n -w -C3 'OutboxEntryState|OutboxEntry|confirm\\(|discard\\(|outbox' | sed -n '1,220p' | rg -n -C5 'packages/chat|outbox|confirm|discard|OutboxEntry' || true
echo "== behavioral probe for current effect logic ==
python3 - <<'PY'
def current_clears(pending, outbox, value, sent_text):
if not pending:
return False
if any(entry == pending for entry in outbox):
return False
if value == sent_text:
return True
return False
for state, present, edited, failed in [
("inflight", True, False, False),
("failed", False, False, False),
("failed", False, True, False),
("queued", False, False, True),
]:
outbox = [pending := {"clientId": "x", "state": state}] if present else []
print(state, "present" if present else "absent", "edited" if edited else "same", "changed" if (outbox and pending["state"]!=state) else "same state", current_clears({"clientId": "x", "sentText": ""}, outbox, "" if not edited else "edited", ""))
PY
echo "== exact diff for current effect around lines 52-61 =="
cat -n "$FILE" | sed -n '48,64p'Repository: superset-sh/superset Length of output: 5847 Clear the composer draft only on confirmed sends.
🤖 Prompt for AI Agents |
||
|
|
||
| const send = () => { | ||
| const text = valueRef.current; | ||
| if (text.trim() === "" || disabled) return; | ||
| const entry = onSend([{ type: "text", text }]); | ||
| if (entry === null) { | ||
| setValue(""); | ||
| window.localStorage.removeItem(draftKey); | ||
| return; | ||
| } | ||
| pendingRef.current = { clientId: entry.clientId, sentText: text }; | ||
| }; | ||
|
|
||
| return ( | ||
| <div className="flex items-end gap-2 border-t border-border p-3"> | ||
| <Textarea | ||
| className="max-h-40 min-h-10 flex-1 resize-none" | ||
| disabled={disabled} | ||
| onChange={(event) => setValue(event.target.value)} | ||
| onKeyDown={(event) => { | ||
| if ( | ||
| event.key === "Enter" && | ||
| !event.shiftKey && | ||
| !event.nativeEvent.isComposing | ||
| ) { | ||
| event.preventDefault(); | ||
| send(); | ||
| } | ||
| }} | ||
| placeholder={placeholder ?? "Message the agent"} | ||
| value={value} | ||
| /> | ||
| {onCancelTurn ? ( | ||
| <Button onClick={onCancelTurn} size="icon" variant="outline"> | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: The send and cancel controls are unlabeled icon-only buttons, so assistive-technology users cannot identify their actions. Add explicit labels such as Prompt for AI agents |
||
| <Square className="size-4" /> | ||
| </Button> | ||
| ) : ( | ||
| <Button | ||
| disabled={disabled || value.trim() === ""} | ||
| onClick={send} | ||
| size="icon" | ||
| > | ||
| <ArrowUp className="size-4" /> | ||
| </Button> | ||
| )} | ||
|
coderabbitai[bot] marked this conversation as resolved.
Resolved
|
||
| </div> | ||
| ); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| export { Composer } from "./Composer"; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,45 @@ | ||
| import { cn } from "@superset/ui/utils"; | ||
| import { memo, useMemo } from "react"; | ||
| import { Streamdown } from "streamdown"; | ||
| import { planMarkdown } from "./utils/planMarkdown"; | ||
|
|
||
| const MarkdownBlock = memo(function MarkdownBlock({ | ||
| block, | ||
| }: { | ||
| block: string; | ||
| }) { | ||
| return ( | ||
| <Streamdown | ||
| className="[&>*:first-child]:mt-0 [&>*:last-child]:mb-0" | ||
| linkSafety={{ enabled: false }} | ||
| mode="streaming" | ||
| > | ||
| {block} | ||
| </Streamdown> | ||
| ); | ||
| }); | ||
|
Comment on lines
+6
to
+20
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win Move
As per coding guidelines, “Use one component per file.” 🤖 Prompt for AI AgentsSource: Coding guidelines |
||
|
|
||
| export function MarkdownView({ | ||
| className, | ||
| text, | ||
| }: { | ||
| text: string; | ||
| className?: string; | ||
| }) { | ||
| const plan = useMemo(() => planMarkdown(text), [text]); | ||
| return ( | ||
| <div className={cn("flex min-w-0 flex-col gap-1 text-sm", className)}> | ||
| {plan.stable.map((entry) => ( | ||
| <MarkdownBlock block={entry.block} key={entry.key} /> | ||
| ))} | ||
| {plan.tail !== null && | ||
| (plan.tailFenceOpen ? ( | ||
| <pre className="overflow-x-auto whitespace-pre-wrap rounded-md bg-muted p-2 font-mono text-xs"> | ||
| {plan.tail} | ||
| </pre> | ||
| ) : ( | ||
| <MarkdownBlock block={plan.tail} /> | ||
| ))} | ||
| </div> | ||
| ); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| export { MarkdownView } from "./MarkdownView"; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| export type { MarkdownBlockPlan, MarkdownPlan } from "./planMarkdown"; | ||
| export { planMarkdown } from "./planMarkdown"; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,38 @@ | ||
| import { describe, expect, test } from "bun:test"; | ||
| import { planMarkdown } from "./planMarkdown"; | ||
|
|
||
| describe("planMarkdown", () => { | ||
| test("stable blocks keep byte-identical keys and content as text streams in", () => { | ||
| const first = planMarkdown("# Title\n\nParagraph one.\n\nParagraph tw"); | ||
| const second = planMarkdown( | ||
| "# Title\n\nParagraph one.\n\nParagraph two.\n\nParagraph three", | ||
| ); | ||
|
|
||
| expect(first.stable.length).toBeGreaterThan(0); | ||
| for (const [index, block] of first.stable.entries()) { | ||
| expect(second.stable[index]).toEqual(block); | ||
| } | ||
| expect(second.tail).toBe("Paragraph three"); | ||
| }); | ||
|
|
||
| test("an open fence marks the tail as plain and closing it releases the block", () => { | ||
| const open = planMarkdown("Intro\n\n```ts\nconst a = 1;\n"); | ||
| expect(open.tailFenceOpen).toBe(true); | ||
| expect(open.tail).toContain("const a = 1;"); | ||
|
|
||
| const closed = planMarkdown("Intro\n\n```ts\nconst a = 1;\n```\n\nAfter"); | ||
| expect(closed.tailFenceOpen).toBe(false); | ||
| expect(closed.tail).toBe("After"); | ||
| expect( | ||
| closed.stable.some((entry) => entry.block.includes("const a = 1;")), | ||
| ).toBe(true); | ||
| }); | ||
|
|
||
| test("empty text renders nothing", () => { | ||
| expect(planMarkdown("")).toEqual({ | ||
| stable: [], | ||
| tail: null, | ||
| tailFenceOpen: false, | ||
| }); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| import { fenceState, splitForStreaming } from "@superset/chat/core"; | ||
|
|
||
| export type MarkdownBlockPlan = { | ||
| key: string; | ||
| block: string; | ||
| }; | ||
|
|
||
| export type MarkdownPlan = { | ||
| stable: MarkdownBlockPlan[]; | ||
| tail: string | null; | ||
| tailFenceOpen: boolean; | ||
| }; | ||
|
|
||
| export function planMarkdown(text: string): MarkdownPlan { | ||
| const { stable, tail } = splitForStreaming(text); | ||
| let offset = 0; | ||
| const keyed = stable.map((block) => { | ||
| const key = `${offset}`; | ||
| offset += block.length; | ||
| return { key, block }; | ||
| }); | ||
| if (tail === "") return { stable: keyed, tail: null, tailFenceOpen: false }; | ||
| return { stable: keyed, tail, tailFenceOpen: fenceState(text).open }; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,56 @@ | ||
| import type { UserContent } from "@superset/chat/protocol"; | ||
| import { Button } from "@superset/ui/button"; | ||
| import type { ReactNode } from "react"; | ||
| import { Composer } from "../Composer"; | ||
|
|
||
| export const HARNESSES = ["claude-code", "codex"] as const; | ||
| export type HarnessId = (typeof HARNESSES)[number]; | ||
|
|
||
| export function NewSessionView({ | ||
| harness, | ||
| headerLeft, | ||
| onHarnessChange, | ||
| onSend, | ||
| workspaceId, | ||
| }: { | ||
| workspaceId: string; | ||
| harness: HarnessId; | ||
| onHarnessChange: (harness: HarnessId) => void; | ||
| onSend: (content: UserContent[]) => void; | ||
| headerLeft?: ReactNode; | ||
| }) { | ||
| return ( | ||
| <div className="flex h-full min-h-0 flex-col"> | ||
| <div className="flex items-center gap-2 border-b border-border px-3 py-2"> | ||
| {headerLeft} | ||
| <div className="ml-auto flex items-center gap-1"> | ||
| {HARNESSES.map((candidate) => ( | ||
| <Button | ||
| key={candidate} | ||
| onClick={() => onHarnessChange(candidate)} | ||
| size="sm" | ||
| variant={candidate === harness ? "secondary" : "ghost"} | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P3: Screen-reader users cannot tell which harness will be used for the next session because selection is conveyed only through the visual button variant. Add Prompt for AI agents |
||
| > | ||
| {candidate} | ||
| </Button> | ||
| ))} | ||
| </div> | ||
| </div> | ||
| <div className="flex flex-1 flex-col items-center justify-center gap-1 text-center"> | ||
| <span className="text-sm font-medium">New chat</span> | ||
| <span className="text-xs text-muted-foreground"> | ||
| Send a message to start a {harness} session | ||
| </span> | ||
| </div> | ||
| <Composer | ||
| draftKey={`chat-v3-draft:new:${workspaceId}`} | ||
| onSend={(content) => { | ||
| onSend(content); | ||
| return null; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P1: A failed new-session request clears the user's draft before the session exists. The send path should retain the draft until session creation and first-prompt delivery are acknowledged, rather than adapting this asynchronous operation to Composer's immediate-success Prompt for AI agents |
||
| }} | ||
|
Comment on lines
+47
to
+50
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When session creation rejects because the host service is starting or unavailable, this callback has already returned Knowledge Base Used: Desktop App (Electron main + renderer) Prompt To Fix With AIThis is a comment left during a code review.
Path: apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/ChatV3Pane/components/NewSessionView/NewSessionView.tsx
Line: 47-50
Comment:
**First prompt clears before creation**
When session creation rejects because the host service is starting or unavailable, this callback has already returned `null`, causing `Composer` to erase the persisted first prompt even though no session or recoverable outbox entry was created.
**Knowledge Base Used:** [Desktop App (Electron main + renderer)](https://app.greptile.com/personal-org-6810/-/custom-context/knowledge-base/superset-sh/superset/-/docs/desktop-app.md)
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly. |
||
| outbox={[]} | ||
| placeholder={`Start a ${harness} session`} | ||
| /> | ||
|
Comment on lines
+45
to
+53
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift Do not clear the draft before session creation succeeds.
Change 🐛 Proposed direction- onSend: (content: UserContent[]) => void;
+ onSend: (content: UserContent[]) => Promise<boolean>; <Composer
draftKey={`chat-v3-draft:new:${workspaceId}`}
- onSend={(content) => {
- onSend(content);
- return null;
- }}
+ onSend={onSend}
outbox={[]}
placeholder={`Start a ${harness} session`}
/>
Based on learnings from the coding guidelines: "Rendered error messages must include 🤖 Prompt for AI AgentsSource: Coding guidelines |
||
| </div> | ||
| ); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| export type { HarnessId } from "./NewSessionView"; | ||
| export { HARNESSES, NewSessionView } from "./NewSessionView"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
Serialize session creation and handle failures.
Each submission creates a new
commandId, but this callback has no pending guard. Rapid submissions can create multiple remote sessions. A rejectedcreateSessioncall is also unhandled because the caller discards its promise. Track creation state, prevent duplicate submission, and catch the failure before updating pane state.🤖 Prompt for AI Agents