@jini-ai/chat 0.6.15-rc.2 → 0.6.16
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 +6 -1
- package/dist/core/accepted-input.js +20 -11
- package/dist/core/ag-ui/entry.js +3 -1
- package/dist/core/agent-plugin-chat-state.js +3 -1
- package/dist/core/antigravity/tool-presentation.d.ts +13 -0
- package/dist/core/antigravity/tool-presentation.js +20 -0
- package/dist/core/assistant-client/ag-ui-transport.js +14 -2
- package/dist/core/assistant-client/attachment-liveness.js +1 -1
- package/dist/core/assistant-client/durable-run-subscription.js +2 -1
- package/dist/core/assistant-client/transport.js +13 -7
- package/dist/core/assistant-content.js +2 -1
- package/dist/core/composer-history.js +2 -1
- package/dist/core/durable-projection.js +2 -1
- package/dist/core/durable-runs/continuation.js +2 -1
- package/dist/core/events.d.ts +5 -1
- package/dist/core/messages.d.ts +5 -1
- package/dist/core/partial-json.js +18 -9
- package/dist/core/persistence/title.d.ts +1 -1
- package/dist/core/persistence/title.js +25 -7
- package/dist/core/question-form/normalize.js +1 -1
- package/dist/core/question-form/scan.js +25 -15
- package/dist/core/question-form/secret-answers.js +2 -1
- package/dist/core/run-activity.js +8 -4
- package/dist/core/run-events/entry.js +6 -2
- package/dist/core/tool-events.d.ts +18 -8
- package/dist/core/tool-events.js +72 -16
- package/dist/core/tool-presentation.d.ts +14 -0
- package/dist/core/tool-presentation.js +8 -0
- package/dist/core/transcript.js +13 -6
- package/dist/core/turn-usage.js +7 -2
- package/dist/core/user-text-redaction.d.ts +4 -6
- package/dist/core/user-text-redaction.js +13 -6
- package/dist/core/util/manifest.js +13 -11
- package/dist/core/util/markdown-context.d.ts +14 -0
- package/dist/core/util/markdown-context.js +42 -4
- package/dist/core/util/markup-attributes.js +1 -1
- package/dist/core/util/parser.js +7 -7
- package/dist/core/util/pointer.js +22 -6
- package/dist/core/util/recover.js +5 -8
- package/dist/core/util/strip.js +12 -15
- package/dist/core/util/validate.js +30 -18
- package/dist/embed-widget/browser/frontend-session-bridge.d.ts +99 -0
- package/dist/embed-widget/browser/frontend-session-bridge.js +384 -0
- package/dist/embed-widget/browser/lazy-loader.js +16 -12
- package/dist/embed-widget/browser/session-store.d.ts +3 -9
- package/dist/embed-widget/browser/session-store.js +6 -38
- package/dist/embed-widget/react/dialog.hooks.js +2 -2
- package/dist/embed-widget/react/embed.d.ts +3 -10
- package/dist/embed-widget/react/embed.js +8 -52
- package/dist/embed-widget/react/header.hooks.js +1 -1
- package/dist/embed-widget/transports/fetch-sse.js +2 -1
- package/dist/embed-widget/vanilla/index.d.ts +0 -5
- package/dist/embed-widget/vanilla/index.js +23 -26
- package/dist/embed-widget/vanilla/state.d.ts +3 -11
- package/dist/embed-widget/vanilla/state.js +10 -39
- package/dist/embed-widget/vanilla/style.css +1 -1
- package/dist/react/agent-bridge/frontend-session-bridge.d.ts +2 -56
- package/dist/react/agent-bridge/frontend-session-bridge.js +2 -248
- package/dist/react/components/AttachmentPreviewModal.hooks.js +2 -1
- package/dist/react/components/ChatSettingsDialog.js +18 -9
- package/dist/react/components/Composer.js +5 -4
- package/dist/react/components/ConversationList.js +5 -4
- package/dist/react/components/Icon.d.ts +1 -1
- package/dist/react/components/Icon.js +1 -0
- package/dist/react/components/Markdown.d.ts +1 -0
- package/dist/react/components/Markdown.js +30 -27
- package/dist/react/components/McpUiSurfaceCard.js +2 -1
- package/dist/react/components/MessageList.d.ts +1 -1
- package/dist/react/components/MessageList.hooks.js +3 -2
- package/dist/react/components/MessageRow.d.ts +3 -0
- package/dist/react/components/MessageRow.js +32 -14
- package/dist/react/components/MessageRow.usage.js +3 -1
- package/dist/react/components/QuestionForm.js +4 -5
- package/dist/react/components/SentAttachment.js +4 -1
- package/dist/react/components/TodoCard.js +9 -1
- package/dist/react/components/ToolCard.d.ts +3 -1
- package/dist/react/components/ToolCard.js +84 -64
- package/dist/react/components/ToolCard.rules.d.ts +20 -0
- package/dist/react/components/ToolCard.rules.js +92 -0
- package/dist/react/components/composer-discovery.js +8 -8
- package/dist/react/features/chat-pane/components/AgentPluginUsage.js +1 -2
- package/dist/react/features/chat-pane/components/AgentRuntimePicker.d.ts +1 -1
- package/dist/react/features/chat-pane/components/AgentRuntimePicker.js +31 -9
- package/dist/react/features/chat-pane/components/ChatPane.js +7 -5
- package/dist/react/features/chat-pane/components/RuntimeDropUp.js +1 -1
- package/dist/react/features/chat-pane/create-daemon-attachment-uploader.js +1 -1
- package/dist/react/features/chat-pane/hooks/useAgentRuntimePicker.hooks.js +2 -2
- package/dist/react/features/chat-pane/hooks/useChatPane.hooks.d.ts +3 -1
- package/dist/react/features/chat-pane/hooks/useChatPane.hooks.js +13 -5
- package/dist/react/features/chat-pane/hooks/useChatPaneAgentControl.hooks.js +2 -2
- package/dist/react/features/chat-pane/hooks/useChatPaneControlsHeight.hooks.js +1 -1
- package/dist/react/features/chat-pane/hooks/useChatPaneRuntimeInventory.hooks.js +2 -2
- package/dist/react/features/chat-pane/hooks/useChatPaneWorkingDirectory.hooks.js +2 -2
- package/dist/react/features/chat-pane/hooks/useRuntimeDropUp.hooks.js +2 -2
- package/dist/react/features/chat-pane/model-options.js +4 -2
- package/dist/react/features/chat-pane/styles.d.ts +1 -1
- package/dist/react/features/chat-pane/styles.js +95 -5
- package/dist/react/features/chat-pane/types.d.ts +11 -0
- package/dist/react/hooks/assistant-chats.js +22 -8
- package/dist/react/hooks/useArtifactStream.js +2 -1
- package/dist/react/hooks/useComposer.js +2 -2
- package/dist/react/hooks/useComposerHistoryKeys.hooks.js +2 -2
- package/dist/react/hooks/useConversation.js +9 -4
- package/dist/react/hooks/useExtEventGroups.js +4 -2
- package/dist/react/hooks/useLatestOperation.js +2 -1
- package/dist/react/hooks/useQuestionForms.js +32 -22
- package/dist/react/hooks/useRunStream.js +4 -2
- package/dist/react/hooks/useUserTextGuard.d.ts +1 -0
- package/dist/react/hooks/useUserTextGuard.js +3 -1
- package/dist/react/message-blocks.js +4 -2
- package/dist/react/styles/reference.css +9 -0
- package/dist/react/tool-call-display.d.ts +11 -0
- package/dist/react/tool-call-display.js +59 -2
- package/dist/server/durable-runs/recover.js +13 -5
- package/dist/server/run-finalizer/entry.js +1 -0
- package/dist/store/runtime/chat-history-store.js +9 -3
- package/dist/store/runtime/durable-run-store.js +34 -24
- package/dist/store/runtime/run-ledger.js +7 -4
- package/dist/store/sql/boundary.js +3 -1
- package/dist/store/sql/cursor.js +8 -3
- package/dist/store/sql/store.js +36 -28
- package/package.json +93 -88
- package/dist/core/browser/page-actions.d.ts +0 -121
- package/dist/core/browser/page-actions.js +0 -115
- package/dist/embed-widget/browser/page-actions.d.ts +0 -2
- package/dist/embed-widget/browser/page-actions.js +0 -3
|
@@ -1,16 +1,15 @@
|
|
|
1
1
|
import { assistantContentFromEvents } from '../../core/assistant-content.js';
|
|
2
|
-
import { extractPageActions, splitPageActions } from '../../core/browser/page-actions.js';
|
|
3
2
|
/**
|
|
4
|
-
* Framework-free visitor state over the existing transport/session
|
|
5
|
-
* Restored messages never replay
|
|
6
|
-
*
|
|
3
|
+
* Framework-free visitor state over the existing transport/session owners.
|
|
4
|
+
* Restored messages never replay effects; page control belongs to the shared frontend bridge.
|
|
5
|
+
* Terminal navigation flushes the transport snapshot before the host tears down the page.
|
|
7
6
|
* Reset invalidates callbacks before aborting, preventing late chunks from reviving a thread.
|
|
8
7
|
* @complexity O(retained transcript + reply events + reply characters) per update; storage writes
|
|
9
8
|
* occur only on settle/open/reset/navigation, never per streamed chunk. Session bounds persisted bytes.
|
|
10
9
|
*/
|
|
11
10
|
export function createVanillaChatController(args, _options = {}) {
|
|
12
11
|
const initial = args.session.load({});
|
|
13
|
-
let state = { ...initial, busy: false, error: null
|
|
12
|
+
let state = { ...initial, busy: false, error: null };
|
|
14
13
|
let generation = 0;
|
|
15
14
|
let active = null;
|
|
16
15
|
const publish = ({ persist = true } = {}) => {
|
|
@@ -18,32 +17,6 @@ export function createVanillaChatController(args, _options = {}) {
|
|
|
18
17
|
args.session.save({ state: { open: state.open, messages: state.messages } });
|
|
19
18
|
args.onChange({ state });
|
|
20
19
|
};
|
|
21
|
-
const go = (proposal) => {
|
|
22
|
-
if (!args.pageActions.allows({ action: proposal.navigate }))
|
|
23
|
-
return;
|
|
24
|
-
if (proposal.bundled && args.pageActions.allows({ action: proposal.bundled }))
|
|
25
|
-
args.session.enqueue({ action: proposal.bundled });
|
|
26
|
-
state = { ...state, proposal: null };
|
|
27
|
-
publish();
|
|
28
|
-
args.pageActions.navigate({ action: proposal.navigate });
|
|
29
|
-
};
|
|
30
|
-
const settleActions = (events) => {
|
|
31
|
-
const actions = extractPageActions({ events, directiveEventName: args.directiveEventName })
|
|
32
|
-
.filter(action => args.pageActions.allows({ action }));
|
|
33
|
-
const { navigate, other } = splitPageActions({ actions });
|
|
34
|
-
if (!navigate) {
|
|
35
|
-
if (other)
|
|
36
|
-
args.pageActions.execute({ action: other });
|
|
37
|
-
return;
|
|
38
|
-
}
|
|
39
|
-
const proposal = { navigate, bundled: other };
|
|
40
|
-
if (navigate.auto)
|
|
41
|
-
go(proposal);
|
|
42
|
-
else {
|
|
43
|
-
state = { ...state, proposal };
|
|
44
|
-
publish();
|
|
45
|
-
}
|
|
46
|
-
};
|
|
47
20
|
return {
|
|
48
21
|
getState(_input = {}, _opts = {}) { return state; },
|
|
49
22
|
setOpen({ open }, _opts = {}) { state = { ...state, open }; publish(); },
|
|
@@ -51,12 +24,13 @@ export function createVanillaChatController(args, _options = {}) {
|
|
|
51
24
|
const content = text.trim();
|
|
52
25
|
if (!content || state.busy)
|
|
53
26
|
return false;
|
|
54
|
-
|
|
27
|
+
generation += 1;
|
|
28
|
+
const token = generation;
|
|
55
29
|
const abort = args.createAbortController({});
|
|
56
30
|
active = abort;
|
|
57
31
|
const history = [...state.messages, { id: args.ids({}), role: 'user', content }];
|
|
58
32
|
const reply = { id: args.ids({}), role: 'assistant', content: '', events: [], runStatus: 'running' };
|
|
59
|
-
state = { ...state, messages: [...history, reply], busy: true, error: null
|
|
33
|
+
state = { ...state, messages: [...history, reply], busy: true, error: null };
|
|
60
34
|
publish({ persist: false });
|
|
61
35
|
let events = [];
|
|
62
36
|
let failed = false;
|
|
@@ -80,10 +54,9 @@ export function createVanillaChatController(args, _options = {}) {
|
|
|
80
54
|
active = null;
|
|
81
55
|
events = final;
|
|
82
56
|
state = { ...state, busy: false };
|
|
57
|
+
const completedStatus = abort.signal.aborted ? 'canceled' : 'succeeded';
|
|
83
58
|
updateReply({ ...reply, content: assistantContentFromEvents({ events }), events: [...events],
|
|
84
|
-
runStatus: failed ? 'failed' :
|
|
85
|
-
if (!failed && !abort.signal.aborted)
|
|
86
|
-
settleActions(events);
|
|
59
|
+
runStatus: failed ? 'failed' : completedStatus }, { persist: true });
|
|
87
60
|
};
|
|
88
61
|
try {
|
|
89
62
|
await args.transport.startRun({ history, agentId: args.agentId, assistantMessageId: reply.id, signal: abort.signal }, {
|
|
@@ -103,15 +76,13 @@ export function createVanillaChatController(args, _options = {}) {
|
|
|
103
76
|
}
|
|
104
77
|
return true;
|
|
105
78
|
},
|
|
106
|
-
acceptProposal(_input = {}, _opts = {}) { if (state.proposal)
|
|
107
|
-
go(state.proposal); },
|
|
108
79
|
cancel(_input = {}, _opts = {}) { active?.abort(); },
|
|
109
80
|
reset(_input = {}, _opts = {}) {
|
|
110
81
|
generation += 1;
|
|
111
82
|
active?.abort();
|
|
112
83
|
active = null;
|
|
113
84
|
args.session.clear({});
|
|
114
|
-
state = { open: state.open, messages: [], busy: false, error: null
|
|
85
|
+
state = { open: state.open, messages: [], busy: false, error: null };
|
|
115
86
|
publish();
|
|
116
87
|
},
|
|
117
88
|
dispose(_input = {}, _opts = {}) { generation += 1; active?.abort(); active = null; },
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
.jini-chat-vanilla :focus-visible{outline:3px solid var(--jini-chat-accent,#2563eb);outline-offset:3px}
|
|
11
11
|
.jini-chat-vanilla{font:14px/1.5 var(--font-body,system-ui,sans-serif);color:var(--jini-chat-text,var(--color-text,CanvasText))}
|
|
12
12
|
.jini-chat-vanilla__panel{position:fixed;right:var(--jini-embed-right,24px);bottom:88px;width:min(var(--jini-embed-width,440px),calc(100vw - 32px));height:min(var(--jini-embed-height,620px),calc(100dvh - 120px));z-index:var(--jini-embed-z-index,1000);display:flex;flex-direction:column;background:var(--jini-chat-background,var(--color-background,Canvas));border:1px solid var(--jini-chat-border,#767676);border-radius:16px;box-shadow:0 12px 48px #0003;overflow:hidden}
|
|
13
|
-
.jini-chat-vanilla__header,.jini-chat-vanilla__composer,.jini-chat-vanilla__confirmation,.jini-chat-
|
|
13
|
+
.jini-chat-vanilla__header,.jini-chat-vanilla__composer,.jini-chat-vanilla__confirmation,.jini-chat-vanilla__tool-confirmation{display:flex;align-items:center;gap:8px;padding:12px;border-bottom:1px solid var(--jini-chat-border,#767676)}
|
|
14
14
|
.jini-chat-vanilla__title{flex:1}
|
|
15
15
|
.jini-chat-vanilla__button{font:inherit;color:inherit;background:transparent;border:1px solid var(--jini-chat-border,#767676);border-radius:8px;padding:6px 10px;cursor:pointer}
|
|
16
16
|
.jini-chat-vanilla__button:disabled{opacity:.5;cursor:default}
|
|
@@ -1,57 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
export interface FrontendSessionBridgeOptions {
|
|
4
|
-
/** Supplying one claims the six `page.*` verbs. Omit for a chat-only surface. */
|
|
5
|
-
readonly pageDriver?: PageDriver;
|
|
6
|
-
/**
|
|
7
|
-
* Product capabilities, keyed by id prefix (`'cms.'`). Each claims every capability under it.
|
|
8
|
-
* This is how a consumer exposes verbs the engine has never heard of.
|
|
9
|
-
*/
|
|
10
|
-
readonly executors?: Readonly<Record<string, (capabilityId: string, input: Record<string, unknown>) => Promise<unknown>>>;
|
|
11
|
-
/** Called for every invocation before it runs — the hook a product uses to show an activity trail. */
|
|
12
|
-
readonly onInvocation?: (action: ChatPaneAgentToolAction) => void;
|
|
13
|
-
/** Reported for failures that are nobody's tool call: stream errors, malformed frames. */
|
|
14
|
-
readonly onError?: (error: unknown) => void;
|
|
15
|
-
}
|
|
16
|
-
/** The host supplies its origin and stream transport; native fetch defaults may be overridden. */
|
|
17
|
-
export interface FrontendSessionBridgeArgs {
|
|
18
|
-
/** Pass an empty string for the same-origin proxy used by a browser host. */
|
|
19
|
-
readonly baseUrl: string;
|
|
20
|
-
readonly openStream: (args: {
|
|
21
|
-
url: string;
|
|
22
|
-
}) => EventSource;
|
|
23
|
-
readonly fetch?: typeof globalThis.fetch;
|
|
24
|
-
readonly request?: (args: {
|
|
25
|
-
url: string;
|
|
26
|
-
init: RequestInit;
|
|
27
|
-
timeoutMs: number;
|
|
28
|
-
}) => Promise<Response>;
|
|
29
|
-
}
|
|
30
|
-
export interface FrontendSessionBridge {
|
|
31
|
-
/** Hand this to `ChatPane`'s `agentControl.bridgeAccess`. */
|
|
32
|
-
readonly bridgeAccess: ChatPaneAgentBridgeAccess;
|
|
33
|
-
/**
|
|
34
|
-
* Resolves the *first* time the daemon attaches this surface — an "is it live yet" signal.
|
|
35
|
-
*
|
|
36
|
-
* Its `bindToken` is only correct until the next reconnect, so do not stash it. Call
|
|
37
|
-
* {@link FrontendSessionBridge.bindToken} when building a run request instead.
|
|
38
|
-
*/
|
|
39
|
-
readonly ready: Promise<{
|
|
40
|
-
sessionId: string;
|
|
41
|
-
bindToken: string;
|
|
42
|
-
}>;
|
|
43
|
-
/**
|
|
44
|
-
* The token for the attachment that is live *now*, or `undefined` before the first attach and
|
|
45
|
-
* after {@link FrontendSessionBridge.close}. Read it at run-start; see the module doc.
|
|
46
|
-
*/
|
|
47
|
-
bindToken(): string | undefined;
|
|
48
|
-
close(): void;
|
|
49
|
-
}
|
|
50
|
-
/**
|
|
51
|
-
* Opens the surface's connection and starts routing invocations.
|
|
52
|
-
*
|
|
53
|
-
* @param options - What this page can serve and where the daemon is.
|
|
54
|
-
* @returns The pane's bridge, the attach result, and a teardown.
|
|
55
|
-
*/
|
|
56
|
-
export declare function createFrontendSessionBridge({ baseUrl: base, openStream, request, fetch }: FrontendSessionBridgeArgs, options?: FrontendSessionBridgeOptions): FrontendSessionBridge;
|
|
1
|
+
/** Compatibility entry; the framework-free browser bridge owns the lifecycle. */
|
|
2
|
+
export * from '../../embed-widget/browser/frontend-session-bridge.js';
|
|
57
3
|
//# sourceMappingURL=frontend-session-bridge.d.ts.map
|
|
@@ -1,249 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
* The browser half of agent-driven frontend control: one SSE connection to the daemon, and the
|
|
5
|
-
* routing that turns each relayed invocation into a call on something in this page.
|
|
6
|
-
*
|
|
7
|
-
* Everything on the server side already existed — `FrontendSessionRegistry` addresses a run to a
|
|
8
|
-
* surface, the frontend-session routes carry invocations out and answers back, and
|
|
9
|
-
* `createFrontendControl` assembles them. `ChatPaneAgentBridgeAccess` declared the contract this
|
|
10
|
-
* satisfies and had **zero implementations**, which is why an agent could reach a tool and the
|
|
11
|
-
* tool could reach nothing.
|
|
12
|
-
*
|
|
13
|
-
* Design notes worth knowing before changing this:
|
|
14
|
-
*
|
|
15
|
-
* - **The connection is owned here, not by `subscribe`.** `ChatPaneAgentBridgeAccess.subscribe` is
|
|
16
|
-
* called by the chat pane's own effect, so tying the stream's lifetime to it would mean page
|
|
17
|
-
* control dies when the pane unmounts — and the page is drivable whether or not a chat pane
|
|
18
|
-
* exists. `subscribe` registers a listener; `close()` tears the connection down.
|
|
19
|
-
* - **Capabilities are claimed from what is actually wired.** The claim sent at connect time is
|
|
20
|
-
* derived from the presence of a `pageDriver` and the keys of `executors`, never from a static
|
|
21
|
-
* list. A surface that claims what it cannot serve turns a fast, named refusal into a timeout.
|
|
22
|
-
* - **An executed invocation is never executed twice.** If the stream drops between the daemon
|
|
23
|
-
* delivering an invocation and this page POSTing the answer, the daemon correctly fails that
|
|
24
|
-
* call — but the click already happened. Without the `executed` set below, the retry clicks
|
|
25
|
-
* again. Buttons are not idempotent.
|
|
26
|
-
* - **The bind token never leaves this module except to the host.** It arrives on the `attached`
|
|
27
|
-
* event and must ride to run-start; it deliberately does not appear in any URL (see
|
|
28
|
-
* `@jini-ai/daemon`'s `FrontendSessionHandle`).
|
|
29
|
-
* - **Read the bind token at send time, not once.** `EventSource` reconnects on its own — after a
|
|
30
|
-
* daemon restart, a sleeping laptop, or an ordinary network blip — and each reattach mints a new
|
|
31
|
-
* session and a new token. A host that captured `ready`'s token keeps sending a dead one, every
|
|
32
|
-
* later run fails to bind, and the only trace is a daemon-side log line: the agent is simply told
|
|
33
|
-
* "no frontend is bound to this run". `ready` therefore answers "has it attached at least once";
|
|
34
|
-
* {@link FrontendSessionBridge.bindToken} answers "what is valid now", and only the latter
|
|
35
|
-
* belongs in a run request.
|
|
36
|
-
*/
|
|
37
|
-
import { CHAT_CAPABILITIES } from '../../core/index.js';
|
|
38
|
-
import { PAGE_CAPABILITIES, executePageCapability } from '@jini-ai/agentic';
|
|
39
|
-
/** How many settled invocation ids to remember for replay suppression. */
|
|
40
|
-
const EXECUTED_MEMORY = 256;
|
|
41
|
-
/** Ids this surface can actually serve, given what the host wired up. */
|
|
42
|
-
function claimedCapabilities(options) {
|
|
43
|
-
const claims = CHAT_CAPABILITIES.map((capability) => capability.id);
|
|
44
|
-
if (options.pageDriver !== undefined) {
|
|
45
|
-
claims.push(...PAGE_CAPABILITIES.map((capability) => capability.id));
|
|
46
|
-
}
|
|
47
|
-
claims.push(...Object.keys(options.executors ?? {}));
|
|
48
|
-
return claims;
|
|
49
|
-
}
|
|
50
|
-
function errorMessage(error) {
|
|
51
|
-
return error instanceof Error ? error.message : String(error);
|
|
52
|
-
}
|
|
53
|
-
/** Parses a frame's data into an object, or says why it is not one. */
|
|
54
|
-
function parseFrameObject(data) {
|
|
55
|
-
let parsed;
|
|
56
|
-
try {
|
|
57
|
-
parsed = JSON.parse(data);
|
|
58
|
-
}
|
|
59
|
-
catch (error) {
|
|
60
|
-
return { error };
|
|
61
|
-
}
|
|
62
|
-
// `JSON.parse` succeeds on any valid JSON value, not just objects — a bare `null`, number,
|
|
63
|
-
// string or boolean parses cleanly and would throw *outside* this try/catch on the very next
|
|
64
|
-
// `frame['type']` read (`null` is the one of those that actually throws; the others just read
|
|
65
|
-
// back `undefined` harmlessly). Guarding here routes every one of those through the same
|
|
66
|
-
// `onError` path as an unparseable frame, rather than only some of them crashing the listener.
|
|
67
|
-
if (parsed === null || typeof parsed !== 'object') {
|
|
68
|
-
return { error: new Error(`malformed frame: expected a JSON object, got ${JSON.stringify(parsed)}`) };
|
|
69
|
-
}
|
|
70
|
-
return { frame: parsed };
|
|
71
|
-
}
|
|
72
|
-
/** Reads an `invocation` frame into the pane's action shape, refusing one with no usable id. */
|
|
73
|
-
function readInvocation(frame) {
|
|
74
|
-
const rawInvocationId = frame['invocationId'];
|
|
75
|
-
if (typeof rawInvocationId !== 'string' || rawInvocationId.length === 0) {
|
|
76
|
-
return { kind: 'malformed', error: new Error('invocation frame missing invocationId') };
|
|
77
|
-
}
|
|
78
|
-
return {
|
|
79
|
-
kind: 'invocation',
|
|
80
|
-
action: {
|
|
81
|
-
invocationId: rawInvocationId,
|
|
82
|
-
capabilityId: String(frame['capabilityId']),
|
|
83
|
-
input: (frame['input'] ?? {}),
|
|
84
|
-
},
|
|
85
|
-
};
|
|
86
|
-
}
|
|
87
|
-
/** Classifies one frame's raw data; the listener only applies the result. */
|
|
88
|
-
function readFrame(data) {
|
|
89
|
-
const parsed = parseFrameObject(data);
|
|
90
|
-
if ('error' in parsed)
|
|
91
|
-
return { kind: 'malformed', error: parsed.error };
|
|
92
|
-
const { frame } = parsed;
|
|
93
|
-
switch (frame['type']) {
|
|
94
|
-
case 'attached':
|
|
95
|
-
return { kind: 'attached', sessionId: String(frame['sessionId']), bindToken: String(frame['bindToken']) };
|
|
96
|
-
case 'error':
|
|
97
|
-
return { kind: 'refused', message: String(frame['message']) };
|
|
98
|
-
case 'invocation':
|
|
99
|
-
return readInvocation(frame);
|
|
100
|
-
default:
|
|
101
|
-
// Unknown frame types are ignored rather than treated as errors, so the daemon's event
|
|
102
|
-
// vocabulary can grow without every deployed surface needing to ship first.
|
|
103
|
-
return { kind: 'ignored' };
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
/**
|
|
107
|
-
* Opens the surface's connection and starts routing invocations.
|
|
108
|
-
*
|
|
109
|
-
* @param options - What this page can serve and where the daemon is.
|
|
110
|
-
* @returns The pane's bridge, the attach result, and a teardown.
|
|
111
|
-
*/
|
|
112
|
-
export function createFrontendSessionBridge({ baseUrl: base, openStream, request, fetch }, options = {}) {
|
|
113
|
-
const capabilities = claimedCapabilities(options);
|
|
114
|
-
// The only caller (`respond`) never passes its own signal, so the deadline is the whole signal.
|
|
115
|
-
const send = request ?? (({ url, init, timeoutMs }) => (fetch ?? globalThis.fetch)(url, { ...init, signal: AbortSignal.timeout(timeoutMs) }));
|
|
116
|
-
let sessionId;
|
|
117
|
-
/** Replaced on every reattach; a promise resolved once could never carry this. */
|
|
118
|
-
let currentBindToken;
|
|
119
|
-
// Definite assignment rather than no-op placeholders: a Promise executor runs synchronously, so
|
|
120
|
-
// both are set before this line finishes, and placeholders would only add two functions that
|
|
121
|
-
// can never be called.
|
|
122
|
-
let resolveReady;
|
|
123
|
-
let rejectReady;
|
|
124
|
-
const ready = new Promise((resolve, reject) => {
|
|
125
|
-
resolveReady = resolve;
|
|
126
|
-
rejectReady = reject;
|
|
127
|
-
});
|
|
128
|
-
/** Chat-pane listeners. The pane registers exactly one; a second host could register another. */
|
|
129
|
-
const chatListeners = new Set();
|
|
130
|
-
/** Invocation ids already run here, so a stream reconnect cannot double-click a button. */
|
|
131
|
-
const executed = new Set();
|
|
132
|
-
const query = capabilities.map((id) => `capability=${encodeURIComponent(id)}`).join('&');
|
|
133
|
-
const source = openStream({ url: `${base}/api/frontend-sessions/stream?${query}` });
|
|
134
|
-
async function respond(invocationId, body) {
|
|
135
|
-
if (sessionId === undefined)
|
|
136
|
-
throw new Error('the surface is not attached yet');
|
|
137
|
-
const response = await send({
|
|
138
|
-
url: `${base}/api/frontend-sessions/${encodeURIComponent(sessionId)}/responses`,
|
|
139
|
-
init: { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ invocationId, ...body }) },
|
|
140
|
-
timeoutMs: 15_000,
|
|
141
|
-
});
|
|
142
|
-
if (!response.ok)
|
|
143
|
-
throw new Error(`answering "${invocationId}" failed: ${response.status}`);
|
|
144
|
-
}
|
|
145
|
-
const respondSuccess = (invocationId, output) => respond(invocationId, { ok: true, output });
|
|
146
|
-
const respondError = (invocationId, message) => respond(invocationId, { ok: false, message });
|
|
147
|
-
/** Runs one non-chat invocation here and answers it. Chat ones go to the pane instead. */
|
|
148
|
-
async function serveLocally(action) {
|
|
149
|
-
let output;
|
|
150
|
-
try {
|
|
151
|
-
if (options.pageDriver !== undefined && action.capabilityId.startsWith('page.')) {
|
|
152
|
-
output = await executePageCapability({ driver: options.pageDriver, capabilityId: action.capabilityId, input: action.input });
|
|
153
|
-
}
|
|
154
|
-
else {
|
|
155
|
-
const entry = Object.entries(options.executors ?? {})
|
|
156
|
-
.find(([prefix]) => action.capabilityId.startsWith(prefix));
|
|
157
|
-
if (entry === undefined)
|
|
158
|
-
throw new Error(`nothing on this page serves "${action.capabilityId}"`);
|
|
159
|
-
output = await entry[1](action.capabilityId, action.input);
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
catch (error) {
|
|
163
|
-
await respondError(action.invocationId, errorMessage(error)).catch(options.onError ?? (() => undefined));
|
|
164
|
-
return;
|
|
165
|
-
}
|
|
166
|
-
await respondSuccess(action.invocationId, output).catch(options.onError ?? (() => undefined));
|
|
167
|
-
}
|
|
168
|
-
function remember(invocationId) {
|
|
169
|
-
executed.add(invocationId);
|
|
170
|
-
if (executed.size > EXECUTED_MEMORY) {
|
|
171
|
-
const oldest = executed.values().next().value;
|
|
172
|
-
if (oldest !== undefined)
|
|
173
|
-
executed.delete(oldest);
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
/** Runs one well-formed invocation: replay check, announcement, then pane or local serving. */
|
|
177
|
-
function handleInvocation(action) {
|
|
178
|
-
// A redelivery after a dropped answer must not re-run the side effect.
|
|
179
|
-
if (executed.has(action.invocationId)) {
|
|
180
|
-
void respondError(action.invocationId, 'already executed on this surface').catch(() => undefined);
|
|
181
|
-
return;
|
|
182
|
-
}
|
|
183
|
-
remember(action.invocationId);
|
|
184
|
-
options.onInvocation?.(action);
|
|
185
|
-
if (action.capabilityId.startsWith('chat.')) {
|
|
186
|
-
for (const listener of chatListeners)
|
|
187
|
-
listener(action);
|
|
188
|
-
return;
|
|
189
|
-
}
|
|
190
|
-
void serveLocally(action);
|
|
191
|
-
}
|
|
192
|
-
source.addEventListener('message', (event) => {
|
|
193
|
-
const frame = readFrame(event.data);
|
|
194
|
-
switch (frame.kind) {
|
|
195
|
-
case 'attached':
|
|
196
|
-
sessionId = frame.sessionId;
|
|
197
|
-
currentBindToken = frame.bindToken;
|
|
198
|
-
// A no-op after the first attach — which is exactly why `currentBindToken` exists.
|
|
199
|
-
resolveReady({ sessionId, bindToken: currentBindToken });
|
|
200
|
-
return;
|
|
201
|
-
case 'refused':
|
|
202
|
-
rejectReady(new Error(frame.message));
|
|
203
|
-
return;
|
|
204
|
-
case 'malformed':
|
|
205
|
-
options.onError?.(frame.error);
|
|
206
|
-
return;
|
|
207
|
-
case 'invocation':
|
|
208
|
-
handleInvocation(frame.action);
|
|
209
|
-
return;
|
|
210
|
-
case 'ignored':
|
|
211
|
-
return;
|
|
212
|
-
}
|
|
213
|
-
});
|
|
214
|
-
source.addEventListener('error', (event) => {
|
|
215
|
-
// The daemon deletes a session's bind token the instant that connection drops, so by the time
|
|
216
|
-
// this fires the token in hand is already gone from the registry. Holding it until the next
|
|
217
|
-
// `attached` frame leaves a window where `bindToken()` hands back something dead, and a run
|
|
218
|
-
// started in that window fails to bind — reported to the agent only as "no frontend is bound
|
|
219
|
-
// to this run", with the real reason (a reconnect it never saw) nowhere in view.
|
|
220
|
-
// Reporting no token is the honest answer for a surface that is, right now, not attached.
|
|
221
|
-
currentBindToken = undefined;
|
|
222
|
-
sessionId = undefined;
|
|
223
|
-
options.onError?.(event);
|
|
224
|
-
});
|
|
225
|
-
const bridgeAccess = {
|
|
226
|
-
subscribe(onAction) {
|
|
227
|
-
chatListeners.add(onAction);
|
|
228
|
-
// Removes this listener only. The connection outlives any one pane — see the module doc.
|
|
229
|
-
return () => chatListeners.delete(onAction);
|
|
230
|
-
},
|
|
231
|
-
respondSuccess,
|
|
232
|
-
respondError,
|
|
233
|
-
};
|
|
234
|
-
return {
|
|
235
|
-
bridgeAccess,
|
|
236
|
-
ready,
|
|
237
|
-
bindToken: () => currentBindToken,
|
|
238
|
-
close() {
|
|
239
|
-
source.close();
|
|
240
|
-
chatListeners.clear();
|
|
241
|
-
// A closed bridge serves nothing, so binding a run to it would strand that run rather than
|
|
242
|
-
// fail it — this is what stops a torn-down surface (a StrictMode remount, a route change)
|
|
243
|
-
// from handing its dead token to the next run.
|
|
244
|
-
currentBindToken = undefined;
|
|
245
|
-
sessionId = undefined;
|
|
246
|
-
},
|
|
247
|
-
};
|
|
248
|
-
}
|
|
1
|
+
/** Compatibility entry; the framework-free browser bridge owns the lifecycle. */
|
|
2
|
+
export * from '../../embed-widget/browser/frontend-session-bridge.js';
|
|
249
3
|
//# sourceMappingURL=frontend-session-bridge.js.map
|
|
@@ -154,7 +154,8 @@ export function useAttachmentPreviewModal({ attachment, onClose, navigation }, {
|
|
|
154
154
|
const cachedFile = getAttachmentPreviewSource({ path: attachment.path });
|
|
155
155
|
const attemptImage = !imageFailed && cachedFile !== undefined && looksLikeImageAttachment({ attachment: attachment }) && canCreateObjectUrl();
|
|
156
156
|
const attemptText = !attemptImage && cachedFile !== undefined && looksLikeTextAttachment(attachment.name);
|
|
157
|
-
const
|
|
157
|
+
const fallbackStatus = attemptText ? 'text' : 'unsupported';
|
|
158
|
+
const status = attemptImage ? 'image' : fallbackStatus;
|
|
158
159
|
// Object URL: created/revoked in an effect (not `useMemo`) so a React 18 Strict Mode double-render
|
|
159
160
|
// can never create two URLs and revoke only one — the cleanup below always pairs with the create
|
|
160
161
|
// that produced the URL currently in state.
|
|
@@ -24,7 +24,7 @@ export function ChatSettingsDialog(required, _optional = {}) {
|
|
|
24
24
|
useLayoutEffect(() => {
|
|
25
25
|
const dialog = dialogRef.current;
|
|
26
26
|
if (!dialog || !required.open)
|
|
27
|
-
return;
|
|
27
|
+
return undefined;
|
|
28
28
|
openerRef.current = document.activeElement;
|
|
29
29
|
if (typeof dialog.showModal === 'function') {
|
|
30
30
|
if (!dialog.open)
|
|
@@ -47,7 +47,7 @@ export function ChatSettingsDialog(required, _optional = {}) {
|
|
|
47
47
|
// layout cleanups and would pull focus straight back to the closing dialog's × button.
|
|
48
48
|
useEffect(() => {
|
|
49
49
|
if (!required.open)
|
|
50
|
-
return;
|
|
50
|
+
return undefined;
|
|
51
51
|
return () => {
|
|
52
52
|
const opener = openerRef.current;
|
|
53
53
|
if (opener instanceof HTMLElement && opener.isConnected)
|
|
@@ -60,13 +60,22 @@ export function ChatSettingsDialog(required, _optional = {}) {
|
|
|
60
60
|
if (event.target !== event.currentTarget)
|
|
61
61
|
return;
|
|
62
62
|
const { clientX: x, clientY: y } = event, box = event.currentTarget.getBoundingClientRect();
|
|
63
|
-
|
|
63
|
+
const horizontalOutside = x < box.left || x > box.right;
|
|
64
|
+
const verticalOutside = y < box.top || y > box.bottom;
|
|
65
|
+
if ((x === 0 && y === 0) || horizontalOutside || verticalOutside)
|
|
64
66
|
required.onClose();
|
|
65
|
-
}, children: [_jsxs("header", { className: "jini-chat-settings-header", children: [_jsx("h2", { id: titleId, className: "jini-dialog-title", children: vm.t('Chat settings') }), _jsx("button", { ref: closeRef, type: "button", className: "jini-dialog-close jini-chat-settings-close", "aria-label": vm.t('Close'), onClick: required.onClose, children: _jsx("span", { "aria-hidden": "true", children: "\u00D7" }) })] }), _jsx(TabBar, { tabs: vm.tabs, activeId: vm.tab, onChange: vm.setTab, ariaLabel: vm.t('Chat settings') }), _jsx("div", { className: "jini-chat-settings-body", role: "tabpanel", "aria-label": vm.tab === 'display' ? vm.t('Display') : vm.t('Agent permissions'), children: vm.tab === 'display' ? vm.display.map(
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
67
|
+
}, children: [_jsxs("header", { className: "jini-chat-settings-header", children: [_jsx("h2", { id: titleId, className: "jini-dialog-title", children: vm.t('Chat settings') }), _jsx("button", { ref: closeRef, type: "button", className: "jini-dialog-close jini-chat-settings-close", "aria-label": vm.t('Close'), onClick: required.onClose, children: _jsx("span", { "aria-hidden": "true", children: "\u00D7" }) })] }), _jsx(TabBar, { tabs: vm.tabs, activeId: vm.tab, onChange: vm.setTab, ariaLabel: vm.t('Chat settings') }), _jsx("div", { className: "jini-chat-settings-body", role: "tabpanel", "aria-label": vm.tab === 'display' ? vm.t('Display') : vm.t('Agent permissions'), children: vm.tab === 'display' ? vm.display.map(renderDisplaySection)
|
|
68
|
+
: renderPermissions(vm) })] });
|
|
69
|
+
}
|
|
70
|
+
function renderDisplaySection(section) {
|
|
71
|
+
if (section.radio)
|
|
72
|
+
return (
|
|
73
|
+
// One ⓘ for the whole choice, beside the legend (owner, 2026-10-18), not one per option. The
|
|
74
|
+
// fieldset is named by the label span alone, so the tip's text never joins the group's name.
|
|
75
|
+
_jsxs("fieldset", { className: "jini-chat-settings-group", "aria-labelledby": section.label ? section.labelId : undefined, "aria-describedby": [section.hint ? section.hintId : '', section.tooltip ? section.tooltipId : ''].filter(Boolean).join(' ') || undefined, children: [section.label && _jsxs("legend", { className: "jini-chat-settings-legend", children: [_jsx("span", { id: section.labelId, children: section.label }), section.tooltip && _jsx(InfoTip, { label: section.tooltip, descriptionId: section.tooltipId })] }), section.hint && _jsx("p", { id: section.hintId, className: "jini-chat-settings-description jini-chat-settings-description--group", children: section.hint }), _jsx(ChatSettingsSegmented, { name: section.name, items: section.items })] }, section.key));
|
|
76
|
+
return _jsxs("fieldset", { className: "jini-chat-settings-group", children: [section.label && _jsx("legend", { className: "jini-chat-settings-legend", children: section.label }), section.items.map(item => _jsx(SettingSwitch, { label: item.label, checked: !!item.checked, disabled: item.disabled, onChange: item.change, description: item.hint, descriptionId: item.hintId, tooltip: item.tooltip }, item.key))] }, section.key);
|
|
77
|
+
}
|
|
78
|
+
function renderPermissions(vm) {
|
|
79
|
+
return _jsxs(_Fragment, { children: [_jsx("p", { className: "jini-chat-settings-note", children: vm.t('Permission changes apply to the next run.') }), vm.permissions.unavailableReason && _jsx("p", { className: "jini-chat-settings-note", children: vm.t(vm.permissions.unavailableReason) }), _jsx(SettingSwitch, { className: "jini-chat-settings-row--master", label: vm.master.label, checked: vm.master.checked, disabled: vm.master.disabled, onChange: checked => vm.master.onCheckedChange({ checked }), description: vm.master.description, descriptionId: vm.master.descriptionId }), _jsx("div", { className: "jini-chat-settings-capabilities", children: vm.capabilities.map(item => _jsx(SettingSwitch, { label: item.control.label, checked: item.control.checked, disabled: item.control.disabled, onChange: checked => item.control.onCheckedChange({ checked }), description: item.description, descriptionId: item.descriptionId }, item.key)) }), vm.permissions.busy && _jsx("p", { role: "status", className: "jini-chat-settings-note", children: vm.t('Saving…') }), vm.permissions.error && _jsx("p", { role: "alert", className: "jini-chat-settings-error", children: vm.permissions.error })] });
|
|
71
80
|
}
|
|
72
81
|
//# sourceMappingURL=ChatSettingsDialog.js.map
|
|
@@ -114,11 +114,11 @@ export function Composer({ composer, onSend, disabled = false, sendDisabled = fa
|
|
|
114
114
|
*/
|
|
115
115
|
useEffect(() => {
|
|
116
116
|
if (!anyPopoverOpen(popovers))
|
|
117
|
-
return;
|
|
117
|
+
return undefined;
|
|
118
118
|
const textarea = textareaRef.current;
|
|
119
119
|
const composerRoot = textarea?.closest('.jini-composer');
|
|
120
120
|
if (!composerRoot)
|
|
121
|
-
return;
|
|
121
|
+
return undefined;
|
|
122
122
|
function handleOutsideMouseDown(event) {
|
|
123
123
|
const target = event.target;
|
|
124
124
|
if (!target)
|
|
@@ -327,7 +327,7 @@ export function Composer({ composer, onSend, disabled = false, sendDisabled = fa
|
|
|
327
327
|
}, onKeyDown: handleKeyDown,
|
|
328
328
|
// Draft persistence is debounced (see `DRAFT_PERSIST_DEBOUNCE_MS`); leaving the field is a
|
|
329
329
|
// natural point to make it durable now.
|
|
330
|
-
onBlur: () => composer.flushDraft?.(), onPaste: handleAttachmentPaste, ...slashTextareaAria(slashOpen, slashActiveOption), rows: 3 }), slashOpen ? (_jsx(ComposerSlashMenu, { matches: slash.matches, activeIndex: slashActiveOption, onSelect: selectSlashItem, showFilterHint: slash.showFilterHint, style: slashMenuPosition, t: t })) : null, _jsxs("div", { className: "jini-composer-footer", children: [_jsx(ComposerAttachButton, { attachmentPicker: attachmentPicker, hidden: hasDiscoveryItems, disabled: disabled, inputRef: attachmentInputRef, onChange: handleAttachmentChange, t: t }), _jsx(ComposerDiscoveryMenu, { groups: discoveryGroups, open: discoveryMenuOpen, disabled: disabled, ...(attachmentPicker ? { attachmentPicker } : {}), attachmentInputRef: attachmentInputRef, onToggle: () => setDiscoveryMenuOpen((open) => !open), onClose: () => setDiscoveryMenuOpen(false), onSelect: selectPlusItem, onAttachmentChange: handleAttachmentChange, style: discoveryMenuPosition, t: t }), _jsx(AccessorySlot, { className: "jini-composer-footer-leading", children: footerLeadingAccessory }), _jsx(ComposerWorkdirControl, { workingDirectory: workingDirectory, onChangeWorkingDirectory: onChangeWorkingDirectory, onPickWorkingDirectory: onPickWorkingDirectory, disabled: disabled, open: workdirOpen, draft: workdirDraft, position: workdirPosition, onToggle: toggleWorkdir, onDraftChange: setWorkdirDraft, onConfirm: confirmWorkdir, onClose: () => setWorkdirOpen(false), t: t }), _jsx(AccessorySlot, { className: "jini-composer-footer-accessories", children: footerAccessories }), _jsx(ComposerPlusMenu, { items: plusMenuItems, t: t }), _jsx(ComposerSendGroup, { running: running, disabled: disabled, sendDisabled: sendDisabled, canSubmit: composer.canSubmit, onSend: onSend, onCancel: onCancel, sendDisabledReason: sendDisabledReason, sendDisabledReasonId: sendDisabledReasonId, t: t })] })] }));
|
|
330
|
+
onBlur: () => composer.flushDraft?.(), onPaste: handleAttachmentPaste, ...slashTextareaAria(slashOpen, slashActiveOption), rows: 3 }), slashOpen ? (_jsx(ComposerSlashMenu, { matches: slash.matches, activeIndex: slashActiveOption, onSelect: selectSlashItem, showFilterHint: slash.showFilterHint, style: slashMenuPosition, t: t })) : null, _jsxs("div", { className: "jini-composer-footer", children: [_jsx(ComposerAttachButton, { attachmentPicker: attachmentPicker, hidden: hasDiscoveryItems, disabled: disabled, inputRef: attachmentInputRef, onChange: handleAttachmentChange, t: t }), _jsx(ComposerDiscoveryMenu, { groups: discoveryGroups, open: discoveryMenuOpen, disabled: disabled, ...(attachmentPicker ? { attachmentPicker } : {}), attachmentInputRef: attachmentInputRef, onToggle: () => setDiscoveryMenuOpen((open) => !open), onClose: () => setDiscoveryMenuOpen(false), onSelect: selectPlusItem, onAttachmentChange: handleAttachmentChange, style: discoveryMenuPosition, t: t }), _jsx(AccessorySlot, { className: "jini-composer-footer-leading", children: footerLeadingAccessory }), _jsx(ComposerWorkdirControl, { workingDirectory: workingDirectory, onChangeWorkingDirectory: onChangeWorkingDirectory, onPickWorkingDirectory: onPickWorkingDirectory, disabled: disabled, open: workdirOpen, draft: workdirDraft, position: workdirPosition, onToggle: toggleWorkdir, onDraftChange: setWorkdirDraft, onConfirm: confirmWorkdir, onClose: () => setWorkdirOpen(false), t: t }), _jsxs("div", { className: "jini-composer-footer-trailing", children: [_jsx(AccessorySlot, { className: "jini-composer-footer-accessories", children: footerAccessories }), _jsx(ComposerPlusMenu, { items: plusMenuItems, t: t }), _jsx(ComposerSendGroup, { running: running, disabled: disabled, sendDisabled: sendDisabled, canSubmit: composer.canSubmit, onSend: onSend, onCancel: onCancel, sendDisabledReason: sendDisabledReason, sendDisabledReasonId: sendDisabledReasonId, t: t })] })] })] }));
|
|
331
331
|
}
|
|
332
332
|
function anyPopoverOpen({ discoveryMenuOpen, slashOpen, workdirOpen }) {
|
|
333
333
|
return discoveryMenuOpen || slashOpen || workdirOpen;
|
|
@@ -380,8 +380,9 @@ function ComposerWorkdirControl({ workingDirectory, onChangeWorkingDirectory, on
|
|
|
380
380
|
if (e.key === 'Enter') {
|
|
381
381
|
e.preventDefault();
|
|
382
382
|
onConfirm();
|
|
383
|
+
return;
|
|
383
384
|
}
|
|
384
|
-
|
|
385
|
+
if (e.key === 'Escape') {
|
|
385
386
|
e.preventDefault();
|
|
386
387
|
onClose();
|
|
387
388
|
}
|
|
@@ -105,7 +105,7 @@ export function ConversationList({ conversations: savedConversations, activeConv
|
|
|
105
105
|
// nothing on every document event.
|
|
106
106
|
useEffect(() => {
|
|
107
107
|
if (!open)
|
|
108
|
-
return;
|
|
108
|
+
return undefined;
|
|
109
109
|
// The root div is committed before any effect runs, so it is never null here. Capturing it
|
|
110
110
|
// (rather than reading the ref per event) keeps a late event during unmount, after React has
|
|
111
111
|
// detached the ref but before this cleanup runs, measured against the real node.
|
|
@@ -135,10 +135,10 @@ export function ConversationList({ conversations: savedConversations, activeConv
|
|
|
135
135
|
}, [open]);
|
|
136
136
|
useEffect(() => {
|
|
137
137
|
if (!onSearch)
|
|
138
|
-
return;
|
|
138
|
+
return undefined;
|
|
139
139
|
if (!deferredQuery) {
|
|
140
140
|
setServerResults(null);
|
|
141
|
-
return;
|
|
141
|
+
return undefined;
|
|
142
142
|
}
|
|
143
143
|
// `cancelled` guards the classic out-of-order response: a slow query for "a" resolving after
|
|
144
144
|
// a fast one for "abc" would otherwise overwrite the newer, correct results.
|
|
@@ -190,6 +190,7 @@ export function ConversationList({ conversations: savedConversations, activeConv
|
|
|
190
190
|
});
|
|
191
191
|
}, [confirmDelete, onDelete, t]);
|
|
192
192
|
const active = conversations.find((c) => c.id === activeConversationId) ?? null;
|
|
193
|
+
const emptyLabel = () => conversations.length === 0 ? emptyState ?? t('No conversations yet') : t('No conversations match');
|
|
193
194
|
return (_jsxs("div", { className: "jini-conv", ref: rootRef, children: [_jsx("button", { type: "button", className: `jini-conv-trigger${open ? ' jini-conv-trigger-open' : ''}`, "aria-haspopup": "menu", "aria-expanded": open, ...conversationTriggerAttributes({ activeTitle: active?.title, t }, { label: triggerLabel, nativeTitle }), "data-testid": "conversation-trigger", onClick: () => setOpen((v) => !v), children: _jsx(Icon, { name: "comment", size: 16 }) }), open ? (_jsxs("div", { className: "jini-conv-menu", role: "menu", "data-testid": "conversation-menu", children: [_jsxs("div", { className: "jini-conv-head", children: [_jsxs("span", { className: "jini-conv-heading", children: [t('Conversations'), _jsx("span", { className: "jini-conv-count", "data-testid": "conversation-count", children: conversations.length })] }), _jsxs("button", { type: "button", className: "jini-conv-new", disabled: createDisabled, "data-testid": "conversation-new",
|
|
194
195
|
// `disabled` alone guards this: a disabled button never dispatches a click.
|
|
195
196
|
onClick: () => {
|
|
@@ -199,7 +200,7 @@ export function ConversationList({ conversations: savedConversations, activeConv
|
|
|
199
200
|
// the remaining gap between "switcher closed" and "the host actually knows".
|
|
200
201
|
runConversationListHostEffect('onCreate', () => onCreate());
|
|
201
202
|
setOpen(false);
|
|
202
|
-
}, children: [_jsx(Icon, { name: "plus", size: 12 }), _jsx("span", { children: t('New') })] })] }), _jsxs("label", { className: "jini-conv-search", children: [_jsx(Icon, { name: "search", size: 13 }), _jsx("input", { type: "search", value: query, placeholder: t('Search conversations'), "data-testid": "conversation-search", onChange: (event) => setQuery(event.currentTarget.value) })] }), _jsx("div", { className: "jini-conv-list", "data-testid": "conversation-list", children: conversations.length === 0
|
|
203
|
+
}, children: [_jsx(Icon, { name: "plus", size: 12 }), _jsx("span", { children: t('New') })] })] }), _jsxs("label", { className: "jini-conv-search", children: [_jsx(Icon, { name: "search", size: 13 }), _jsx("input", { type: "search", value: query, placeholder: t('Search conversations'), "data-testid": "conversation-search", onChange: (event) => setQuery(event.currentTarget.value) })] }), _jsx("div", { className: "jini-conv-list", "data-testid": "conversation-list", children: conversations.length === 0 || visible.length === 0 ? (_jsx("div", { className: "jini-conv-empty", children: emptyLabel() })) : (visible.map((item) => (_jsxs("div", { className: `jini-conv-item${item.id === activeConversationId ? ' jini-conv-item-active' : ''}`, "data-testid": `conversation-item-${item.id}`, children: [editingId === item.id ? (_jsx(ConversationTitleEditor, { title: rawTitles.get(item.id) ?? null, "data-testid": `conversation-rename-${item.id}`, onCommit: (next) => commitRename(item.id, next), onCancel: () => setEditingId(null) })) : (_jsxs("button", { type: "button", className: "jini-conv-item-select", "data-testid": `conversation-select-${item.id}`, title: t('Double-click to rename'), onClick: () => {
|
|
203
204
|
cancelPendingSelect();
|
|
204
205
|
pendingSelectRef.current = setTimeout(() => {
|
|
205
206
|
pendingSelectRef.current = null;
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* AgentIcon and WorkingDirPicker.
|
|
8
8
|
*/
|
|
9
9
|
import type { SVGProps } from 'react';
|
|
10
|
-
export type IconName = 'spinner' | 'check' | 'close' | 'chevron-down' | 'chevron-left' | 'chevron-right' | 'attach' | 'file' | 'image' | 'x' | 'comment' | 'search' | 'plus' | 'pencil' | 'copy';
|
|
10
|
+
export type IconName = 'spinner' | 'check' | 'close' | 'chevron-down' | 'chevron-left' | 'chevron-right' | 'attach' | 'file' | 'image' | 'x' | 'comment' | 'search' | 'plus' | 'pencil' | 'copy' | 'retry';
|
|
11
11
|
interface IconProps extends Omit<SVGProps<SVGSVGElement>, 'name'> {
|
|
12
12
|
name: IconName;
|
|
13
13
|
size?: number | string;
|
|
@@ -6,6 +6,7 @@ const ICON_GLYPHS = {
|
|
|
6
6
|
check: _jsx("path", { d: "M20 6 9 17l-5-5" }),
|
|
7
7
|
close: CROSS,
|
|
8
8
|
x: CROSS,
|
|
9
|
+
retry: _jsxs(_Fragment, { children: [_jsx("path", { d: "M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8" }), _jsx("path", { d: "M3 3v5h5" })] }),
|
|
9
10
|
'chevron-down': _jsx("path", { d: "m6 9 6 6 6-6" }),
|
|
10
11
|
/** Conversation-switcher trigger — a speech bubble reads as "chat history" without a label. */
|
|
11
12
|
comment: (_jsx("path", { d: "M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z" })),
|