@genesislcap/ai-assistant 14.452.1 → 14.453.0
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/dist/ai-assistant.api.json +221 -2
- package/dist/ai-assistant.d.ts +93 -1
- package/dist/dts/components/ai-driver/ai-driver.d.ts +13 -0
- package/dist/dts/components/ai-driver/ai-driver.d.ts.map +1 -1
- package/dist/dts/components/chat-driver/chat-driver.d.ts +47 -0
- package/dist/dts/components/chat-driver/chat-driver.d.ts.map +1 -1
- package/dist/dts/components/orchestrating-driver/orchestrating-driver.d.ts +12 -0
- package/dist/dts/components/orchestrating-driver/orchestrating-driver.d.ts.map +1 -1
- package/dist/dts/main/main.d.ts +16 -0
- package/dist/dts/main/main.d.ts.map +1 -1
- package/dist/dts/main/main.styles.d.ts.map +1 -1
- package/dist/dts/main/main.template.d.ts.map +1 -1
- package/dist/dts/main/main.types.d.ts +5 -1
- package/dist/dts/main/main.types.d.ts.map +1 -1
- package/dist/esm/components/chat-driver/chat-driver.js +129 -1
- package/dist/esm/components/chat-driver/chat-driver.test.js +190 -0
- package/dist/esm/components/orchestrating-driver/orchestrating-driver.js +23 -0
- package/dist/esm/main/main.js +49 -11
- package/dist/esm/main/main.styles.js +26 -0
- package/dist/esm/main/main.template.js +27 -11
- package/package.json +16 -16
- package/src/components/ai-driver/ai-driver.ts +15 -0
- package/src/components/chat-driver/chat-driver.test.ts +227 -0
- package/src/components/chat-driver/chat-driver.ts +136 -1
- package/src/components/orchestrating-driver/orchestrating-driver.ts +24 -0
- package/src/main/main.styles.ts +26 -0
- package/src/main/main.template.ts +30 -11
- package/src/main/main.ts +48 -11
- package/src/main/main.types.ts +5 -1
|
@@ -93,6 +93,12 @@ export class OrchestratingDriver extends EventTarget implements AiDriver {
|
|
|
93
93
|
* so long-running `onActivate` work can bail if the session disconnects.
|
|
94
94
|
*/
|
|
95
95
|
private readonly lifecycleAbortController = new AbortController();
|
|
96
|
+
/**
|
|
97
|
+
* Set by `cancel()` for the duration of a `sendMessage` run. Breaks the
|
|
98
|
+
* handoff loop so we don't classify or start another agent turn after the
|
|
99
|
+
* user stops. Reset at the start of each `sendMessage`.
|
|
100
|
+
*/
|
|
101
|
+
private cancelled = false;
|
|
96
102
|
/**
|
|
97
103
|
* Sticky user pick from the picker (or the host's `setAgent` API). Only
|
|
98
104
|
* changes on explicit user action. Survives flow completion: when a stateful
|
|
@@ -263,6 +269,7 @@ export class OrchestratingDriver extends EventTarget implements AiDriver {
|
|
|
263
269
|
}
|
|
264
270
|
|
|
265
271
|
async sendMessage(input: string, attachments?: ChatAttachment[]): Promise<ChatDriverResult> {
|
|
272
|
+
this.cancelled = false;
|
|
266
273
|
const history = this.chatDriver.getHistory() as ChatMessage[];
|
|
267
274
|
|
|
268
275
|
// Emit the user message immediately so the UI reflects it during the classify
|
|
@@ -282,6 +289,11 @@ export class OrchestratingDriver extends EventTarget implements AiDriver {
|
|
|
282
289
|
let remainingTask = '';
|
|
283
290
|
|
|
284
291
|
while (true) {
|
|
292
|
+
// Cancelled before a (next) agent turn started — e.g. during classify.
|
|
293
|
+
// The chat driver appends its own "Stopped." marker when a turn it was
|
|
294
|
+
// running is cancelled, so we just stop here without a duplicate.
|
|
295
|
+
if (this.cancelled) break;
|
|
296
|
+
|
|
285
297
|
// oxlint-disable-next-line no-await-in-loop
|
|
286
298
|
await this.applyAgent(currentAgent);
|
|
287
299
|
|
|
@@ -471,6 +483,16 @@ export class OrchestratingDriver extends EventTarget implements AiDriver {
|
|
|
471
483
|
this.dispatchEvent(new CustomEvent('agent-changed', { detail: undefined }));
|
|
472
484
|
}
|
|
473
485
|
|
|
486
|
+
/**
|
|
487
|
+
* Stop the current turn (user "stop" button). Cancels the inner chat driver's
|
|
488
|
+
* in-flight request and breaks the handoff loop so no further agent turn or
|
|
489
|
+
* classify runs. The driver stays usable for the next message.
|
|
490
|
+
*/
|
|
491
|
+
cancel(): void {
|
|
492
|
+
this.cancelled = true;
|
|
493
|
+
this.chatDriver.cancel();
|
|
494
|
+
}
|
|
495
|
+
|
|
474
496
|
/**
|
|
475
497
|
* Fire `onDeactivate` on the current active agent and abort any pending
|
|
476
498
|
* lifecycle work. Called by the host on session teardown so machines can
|
|
@@ -490,6 +512,8 @@ export class OrchestratingDriver extends EventTarget implements AiDriver {
|
|
|
490
512
|
}
|
|
491
513
|
}
|
|
492
514
|
this.lifecycleAbortController.abort();
|
|
515
|
+
// Tear down the inner driver too, so any in-flight provider request aborts.
|
|
516
|
+
this.chatDriver.dispose();
|
|
493
517
|
this.activeAgent = undefined;
|
|
494
518
|
}
|
|
495
519
|
|
package/src/main/main.styles.ts
CHANGED
|
@@ -599,6 +599,32 @@ export const styles = css`
|
|
|
599
599
|
background-color: var(--neutral-layer-2);
|
|
600
600
|
}
|
|
601
601
|
|
|
602
|
+
/* Right-hand control column, mirroring .input-left-controls — holds the
|
|
603
|
+
Stop button (shown while a turn runs) stacked above the Send button. The
|
|
604
|
+
Stop button is an icon (stop → spinner), so the column width is driven by
|
|
605
|
+
Send and stays stable whether a turn is running or idle. */
|
|
606
|
+
.input-right-controls {
|
|
607
|
+
display: flex;
|
|
608
|
+
flex-direction: column;
|
|
609
|
+
align-items: stretch;
|
|
610
|
+
gap: calc(var(--design-unit) * 1px);
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
/* The "stopping" state shows a spinner — rotate it (the icon set's glyph is
|
|
614
|
+
static). Transform-only, so it never affects layout/width. */
|
|
615
|
+
@keyframes ai-stop-spin {
|
|
616
|
+
to {
|
|
617
|
+
transform: rotate(360deg);
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
.stop-icon-spinning {
|
|
622
|
+
/* inline-block so the icon host is transformable — an inline element
|
|
623
|
+
ignores the transform, which is why the spinner appeared frozen. */
|
|
624
|
+
display: inline-block;
|
|
625
|
+
animation: ai-stop-spin 0.8s linear infinite;
|
|
626
|
+
}
|
|
627
|
+
|
|
602
628
|
.input-left-controls {
|
|
603
629
|
display: flex;
|
|
604
630
|
flex-direction: column;
|
|
@@ -22,7 +22,7 @@ import type {
|
|
|
22
22
|
ChatToolCall,
|
|
23
23
|
} from '@genesislcap/foundation-ai';
|
|
24
24
|
import { isChatToolCallUnknown } from '@genesislcap/foundation-ai';
|
|
25
|
-
import { html, ref, repeat, when, ViewTemplate } from '@genesislcap/web-core';
|
|
25
|
+
import { classNames, html, ref, repeat, when, ViewTemplate } from '@genesislcap/web-core';
|
|
26
26
|
import type { FoundationAiAssistant } from './main';
|
|
27
27
|
import { ANIMATION_DEFS } from './main.types';
|
|
28
28
|
|
|
@@ -288,7 +288,7 @@ ${(tc) => (tc.foldPath?.length ? `${tc.foldPath.join(' › ')} › ` : '')}<stro
|
|
|
288
288
|
part="agent-toggle-button"
|
|
289
289
|
appearance="stealth"
|
|
290
290
|
title=${(x) => x.agentToggleTitle}
|
|
291
|
-
?disabled=${(x) => x.
|
|
291
|
+
?disabled=${(x) => x.busy || x.pinLocked}
|
|
292
292
|
@click=${(x) => x.toggleAgentPicker()}
|
|
293
293
|
>
|
|
294
294
|
${when(
|
|
@@ -319,7 +319,7 @@ ${(tc) => (tc.foldPath?.length ? `${tc.foldPath.join(' › ')} › ` : '')}<stro
|
|
|
319
319
|
part="attach-button"
|
|
320
320
|
appearance="stealth"
|
|
321
321
|
title=${(x) => `Attach file (${x.chatConfig.ui?.acceptedFiles})`}
|
|
322
|
-
?disabled=${(x) => x.
|
|
322
|
+
?disabled=${(x) => x.busy}
|
|
323
323
|
@click=${(x) => x.triggerFileInput()}
|
|
324
324
|
><${iconTag} name="paperclip"></${iconTag}></${buttonTag}>
|
|
325
325
|
`;
|
|
@@ -658,7 +658,7 @@ ${(tc) => (tc.foldPath?.length ? `${tc.foldPath.join(' › ')} › ` : '')}<stro
|
|
|
658
658
|
${when(
|
|
659
659
|
(x) =>
|
|
660
660
|
!x.composerHiddenByConfig &&
|
|
661
|
-
!(x.
|
|
661
|
+
!(x.busy && x.effectiveChatInputDuringExecution === 'hidden'),
|
|
662
662
|
html<FoundationAiAssistant>`
|
|
663
663
|
<div class="input-row" part="input-row">
|
|
664
664
|
${when(
|
|
@@ -671,7 +671,7 @@ ${(tc) => (tc.foldPath?.length ? `${tc.foldPath.join(' › ')} › ` : '')}<stro
|
|
|
671
671
|
part="input"
|
|
672
672
|
placeholder=${(x) => x.effectivePlaceholder}
|
|
673
673
|
:value=${(x) => x.inputValue}
|
|
674
|
-
?disabled=${(x) => x.
|
|
674
|
+
?disabled=${(x) => x.busy}
|
|
675
675
|
@input=${(x, c) => (x.inputValue = (c.event.target as any).value)}
|
|
676
676
|
@keydown=${(x, c) => {
|
|
677
677
|
if (
|
|
@@ -685,12 +685,31 @@ ${(tc) => (tc.foldPath?.length ? `${tc.foldPath.join(' › ')} › ` : '')}<stro
|
|
|
685
685
|
return true;
|
|
686
686
|
}}
|
|
687
687
|
></${textareaTag}>
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
688
|
+
<div class="input-right-controls">
|
|
689
|
+
${when(
|
|
690
|
+
(x) => x.busy && !x.chatConfig.ui?.hideStopButton,
|
|
691
|
+
html<FoundationAiAssistant>`
|
|
692
|
+
<${buttonTag}
|
|
693
|
+
class="stop-button"
|
|
694
|
+
part="stop-button"
|
|
695
|
+
appearance="stealth"
|
|
696
|
+
title="${(x) => (x.state === 'cancelling' ? 'Stopping…' : 'Stop')}"
|
|
697
|
+
aria-label="${(x) => (x.state === 'cancelling' ? 'Stopping' : 'Stop')}"
|
|
698
|
+
?disabled=${(x) => x.state === 'cancelling'}
|
|
699
|
+
@click=${(x) => x.handleStopClick()}
|
|
700
|
+
><${iconTag}
|
|
701
|
+
class="${(x) => classNames('stop-icon', ['stop-icon-spinning', x.state === 'cancelling'])}"
|
|
702
|
+
name="${(x) => (x.state === 'cancelling' ? 'spinner' : 'stop')}"
|
|
703
|
+
></${iconTag}></${buttonTag}>
|
|
704
|
+
`,
|
|
705
|
+
)}
|
|
706
|
+
<${buttonTag}
|
|
707
|
+
class="send-button"
|
|
708
|
+
part="send-button"
|
|
709
|
+
?disabled=${(x) => x.busy || (!x.inputValue.trim() && !x.attachments.length)}
|
|
710
|
+
@click=${(x) => x.handleSendClick()}
|
|
711
|
+
>Send</${buttonTag}>
|
|
712
|
+
</div>
|
|
694
713
|
</div>
|
|
695
714
|
`,
|
|
696
715
|
)}
|
package/src/main/main.ts
CHANGED
|
@@ -241,10 +241,11 @@ export class FoundationAiAssistant extends GenesisElement {
|
|
|
241
241
|
this.logMeta('state.changed', { from: prev, to: value });
|
|
242
242
|
}
|
|
243
243
|
this.syncShowHalo();
|
|
244
|
-
// When the
|
|
245
|
-
// becomes enabled) — refocus it so the user can type
|
|
246
|
-
// if they haven't moved on to something else
|
|
247
|
-
|
|
244
|
+
// When the turn finishes (a busy state → a settled one) the input row
|
|
245
|
+
// reappears (or becomes enabled) — refocus it so the user can type
|
|
246
|
+
// immediately, but only if they haven't moved on to something else.
|
|
247
|
+
// `cancelling` is busy too, so loading → cancelling must NOT settle yet.
|
|
248
|
+
if (this.isBusyState(prev) && !this.isBusyState(value)) {
|
|
248
249
|
this.maybeAutoFocusChatInput();
|
|
249
250
|
// Apply any agents swap deferred by the busy guard in `agentsChanged`.
|
|
250
251
|
// Host-side `agents` getters typically cache a stable reference between
|
|
@@ -254,6 +255,23 @@ export class FoundationAiAssistant extends GenesisElement {
|
|
|
254
255
|
}
|
|
255
256
|
}
|
|
256
257
|
|
|
258
|
+
/** Whether a state value counts as "busy" — a turn is active. */
|
|
259
|
+
private isBusyState(s?: AiAssistantState): boolean {
|
|
260
|
+
return s === 'loading' || s === 'cancelling';
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* True while a turn is active — `loading` or `cancelling` (a stop was
|
|
265
|
+
* requested but the turn is still winding down). The single "is a turn
|
|
266
|
+
* running" predicate; UI gates use this so `cancelling` behaves like
|
|
267
|
+
* `loading` everywhere. `@volatile` so the template re-evaluates it when the
|
|
268
|
+
* store-backed `state` changes.
|
|
269
|
+
*/
|
|
270
|
+
@volatile
|
|
271
|
+
get busy(): boolean {
|
|
272
|
+
return this.isBusyState(this.state);
|
|
273
|
+
}
|
|
274
|
+
|
|
257
275
|
/**
|
|
258
276
|
* Re-runs `agentsChanged` if the live `agents` array no longer matches the
|
|
259
277
|
* fingerprint of the currently installed driver. Used to apply swaps that
|
|
@@ -549,7 +567,7 @@ export class FoundationAiAssistant extends GenesisElement {
|
|
|
549
567
|
@observable showHalo: 'no' | 'orchestrating' | 'agent' = 'no';
|
|
550
568
|
|
|
551
569
|
private syncShowHalo() {
|
|
552
|
-
if (this.
|
|
570
|
+
if (!this.busy) {
|
|
553
571
|
this.showHalo = 'no';
|
|
554
572
|
return;
|
|
555
573
|
}
|
|
@@ -563,7 +581,7 @@ export class FoundationAiAssistant extends GenesisElement {
|
|
|
563
581
|
|
|
564
582
|
/** True when there is a pending (unresolved) interaction — disables the popout button. */
|
|
565
583
|
get hasActivePendingInteraction(): boolean {
|
|
566
|
-
if (this.
|
|
584
|
+
if (!this.busy) return false;
|
|
567
585
|
const last = this.messages[this.messages.length - 1];
|
|
568
586
|
return !!last?.interaction && !last.interaction.resolved;
|
|
569
587
|
}
|
|
@@ -1020,7 +1038,7 @@ export class FoundationAiAssistant extends GenesisElement {
|
|
|
1020
1038
|
document.addEventListener('focusin', this._handleGlobalInteraction, true);
|
|
1021
1039
|
// Initial focus on mount when idle (skips during a mid-lifecycle reattach
|
|
1022
1040
|
// where state is loading and the input may be hidden or disabled).
|
|
1023
|
-
if (this.
|
|
1041
|
+
if (!this.busy) this.maybeAutoFocusChatInput();
|
|
1024
1042
|
logger.debug('FoundationAiAssistant connected');
|
|
1025
1043
|
this.logMeta('assistant.connected', {
|
|
1026
1044
|
store: isNewStore ? 'created' : 'restored',
|
|
@@ -1112,7 +1130,7 @@ export class FoundationAiAssistant extends GenesisElement {
|
|
|
1112
1130
|
// individual step gets a fresh window before the spinner appears.
|
|
1113
1131
|
// If the last message is a blocking interaction, stop the timer — the AI is
|
|
1114
1132
|
// waiting for the user, not computing.
|
|
1115
|
-
if (this.
|
|
1133
|
+
if (this.busy) {
|
|
1116
1134
|
const last = this.messages[this.messages.length - 1];
|
|
1117
1135
|
if (last?.interaction) {
|
|
1118
1136
|
this.stopLoadingTimer();
|
|
@@ -1641,6 +1659,18 @@ export class FoundationAiAssistant extends GenesisElement {
|
|
|
1641
1659
|
this.send();
|
|
1642
1660
|
}
|
|
1643
1661
|
|
|
1662
|
+
/**
|
|
1663
|
+
* User clicked Stop. Cancels the current turn via the driver and flips the
|
|
1664
|
+
* button to "Stopping…" until the turn winds down (a tool mid-execution
|
|
1665
|
+
* finishes first). Guarded so a second click is a no-op.
|
|
1666
|
+
*/
|
|
1667
|
+
handleStopClick() {
|
|
1668
|
+
// Only valid mid-turn; once `cancelling`, a second click is a no-op.
|
|
1669
|
+
if (this.state !== 'loading') return;
|
|
1670
|
+
this.state = 'cancelling';
|
|
1671
|
+
this.driver?.cancel?.();
|
|
1672
|
+
}
|
|
1673
|
+
|
|
1644
1674
|
handleSuggestionClick(suggestion: string) {
|
|
1645
1675
|
this.inputValue = suggestion;
|
|
1646
1676
|
this.send();
|
|
@@ -1668,7 +1698,7 @@ export class FoundationAiAssistant extends GenesisElement {
|
|
|
1668
1698
|
* @beta
|
|
1669
1699
|
*/
|
|
1670
1700
|
async submitMessage(input: { text?: string; files?: File[] }): Promise<SubmitMessageResult> {
|
|
1671
|
-
if (this.
|
|
1701
|
+
if (this.busy) {
|
|
1672
1702
|
return { ok: false, errors: ['Assistant is busy'] };
|
|
1673
1703
|
}
|
|
1674
1704
|
|
|
@@ -1682,7 +1712,7 @@ export class FoundationAiAssistant extends GenesisElement {
|
|
|
1682
1712
|
// own first await — so no further race window exists.
|
|
1683
1713
|
// (Cast widens through TS's narrowing of `this.state` from the earlier
|
|
1684
1714
|
// early-return; the getter can return a different value across an await.)
|
|
1685
|
-
if (
|
|
1715
|
+
if (this.busy) {
|
|
1686
1716
|
return { ok: false, errors: ['Assistant is busy'] };
|
|
1687
1717
|
}
|
|
1688
1718
|
if (errors.length) {
|
|
@@ -1772,7 +1802,7 @@ export class FoundationAiAssistant extends GenesisElement {
|
|
|
1772
1802
|
|
|
1773
1803
|
private async send() {
|
|
1774
1804
|
const input = this.inputValue.trim();
|
|
1775
|
-
if ((!input && !this.attachments.length) || this.
|
|
1805
|
+
if ((!input && !this.attachments.length) || this.busy) return;
|
|
1776
1806
|
// Capture the session ref before any await. If a lifecycle event occurs during
|
|
1777
1807
|
// execution, disconnectedCallback clears this._sessionRef — but the captured
|
|
1778
1808
|
// reference lets the finally block still write 'idle' back to the shared store,
|
|
@@ -1800,7 +1830,14 @@ export class FoundationAiAssistant extends GenesisElement {
|
|
|
1800
1830
|
// Write directly via captured ref — this._sessionRef may be null if the element
|
|
1801
1831
|
// disconnected mid-execution. The state setter also calls syncShowHalo(); replicate
|
|
1802
1832
|
// that here for the case where this element is still connected.
|
|
1833
|
+
const prevState = capturedSessionRef?.store.aiAssistant.state;
|
|
1803
1834
|
capturedSessionRef?.actions.aiAssistant.setState('idle');
|
|
1835
|
+
// The setter's `state.changed` log is bypassed on this direct write, so
|
|
1836
|
+
// emit it here — otherwise the debug timeline shows the turn entering
|
|
1837
|
+
// loading/cancelling but never returning to idle.
|
|
1838
|
+
if (prevState && prevState !== 'idle') {
|
|
1839
|
+
this.logMeta('state.changed', { from: prevState, to: 'idle' });
|
|
1840
|
+
}
|
|
1804
1841
|
this.syncShowHalo();
|
|
1805
1842
|
this.fetchSuggestions();
|
|
1806
1843
|
// setState was dispatched directly via the captured ref (not via the
|
package/src/main/main.types.ts
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* State of the AI assistant component.
|
|
3
3
|
*
|
|
4
|
+
* `cancelling` is a refinement of `loading`: a stop was requested but the turn
|
|
5
|
+
* is still winding down (e.g. a tool mid-execution finishing). It is "busy"
|
|
6
|
+
* like `loading` — use the host's `busy` getter for "is a turn active" gates.
|
|
7
|
+
*
|
|
4
8
|
* @beta
|
|
5
9
|
*/
|
|
6
|
-
export type AiAssistantState = 'idle' | 'loading' | 'error';
|
|
10
|
+
export type AiAssistantState = 'idle' | 'loading' | 'cancelling' | 'error';
|
|
7
11
|
|
|
8
12
|
/**
|
|
9
13
|
* Controls the pop-out button shown in the assistant header.
|