@adecore/agents-react 0.24.2 → 0.25.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/chat/index.d.ts +4 -7
- package/dist/chat/index.js +4 -7
- package/dist/chat/logic/continue-after-stop.d.ts +3 -0
- package/dist/chat/logic/continue-after-stop.js +28 -0
- package/dist/chat/logic/thread-cards.d.ts +0 -1
- package/dist/chat/logic/thread-cards.js +0 -4
- package/dist/chat/preferences.d.ts +0 -1
- package/dist/chat/preferences.js +0 -3
- package/dist/chat/stash.d.ts +6 -0
- package/dist/chat/stash.js +13 -0
- package/dist/chat/ui/Composer.js +26 -5
- package/dist/chat/ui/Pickers.js +1 -1
- package/dist/chat/ui/composer/ComposerControls.d.ts +1 -0
- package/dist/chat/ui/composer/ComposerControls.js +3 -3
- package/dist/chat/ui/composer/use-composer-submit.d.ts +3 -1
- package/dist/chat/ui/composer/use-composer-submit.js +22 -4
- package/dist/chat/ui/composer/use-continuable-turn.d.ts +2 -0
- package/dist/chat/ui/composer/use-continuable-turn.js +12 -0
- package/dist/chat/ui/composer/use-notice.d.ts +3 -1
- package/dist/chat/ui/composer/use-notice.js +8 -6
- package/dist/chat/ui/index.d.ts +9 -28
- package/dist/chat/ui/index.js +8 -27
- package/dist/chat/ui/intelligent-ui/UiReply.js +20 -2
- package/dist/chat/ui/intelligent-ui/node-text.d.ts +3 -1
- package/dist/chat/ui/intelligent-ui/node-text.js +32 -0
- package/dist/chat/ui/intelligent-ui/registry.js +15 -5
- package/dist/chat/ui/intelligent-ui/render-context.d.ts +6 -0
- package/dist/chat/ui/intelligent-ui/renderers/base-land.d.ts +1 -0
- package/dist/chat/ui/intelligent-ui/renderers/base-land.js +6 -0
- package/dist/chat/ui/intelligent-ui/renderers/binding.d.ts +6 -0
- package/dist/chat/ui/intelligent-ui/renderers/binding.js +10 -0
- package/dist/chat/ui/intelligent-ui/renderers/content.d.ts +2 -0
- package/dist/chat/ui/intelligent-ui/renderers/content.js +68 -6
- package/dist/chat/ui/intelligent-ui/renderers/inputs.d.ts +2 -0
- package/dist/chat/ui/intelligent-ui/renderers/inputs.js +40 -13
- package/dist/chat/ui/intelligent-ui/renderers/map-slots.d.ts +20 -0
- package/dist/chat/ui/intelligent-ui/renderers/map-slots.js +87 -0
- package/dist/chat/ui/intelligent-ui/renderers/map-style.d.ts +18 -0
- package/dist/chat/ui/intelligent-ui/renderers/map-style.js +72 -0
- package/dist/chat/ui/intelligent-ui/renderers/map-view.d.ts +15 -0
- package/dist/chat/ui/intelligent-ui/renderers/map-view.js +213 -0
- package/dist/chat/ui/intelligent-ui/renderers/map.d.ts +3 -0
- package/dist/chat/ui/intelligent-ui/renderers/map.js +113 -0
- package/dist/chat/ui/intelligent-ui/renderers/place-point.d.ts +3 -0
- package/dist/chat/ui/intelligent-ui/renderers/place-point.js +13 -0
- package/dist/chat/ui/intelligent-ui/renderers/text.d.ts +2 -0
- package/dist/chat/ui/intelligent-ui/renderers/text.js +14 -1
- package/dist/chat/ui/intelligent-ui/use-in-view.d.ts +1 -0
- package/dist/chat/ui/intelligent-ui/use-in-view.js +17 -0
- package/dist/host.d.ts +16 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/locales/en/agent-chat.d.ts +47 -0
- package/dist/locales/en/agent-chat.js +48 -1
- package/dist/locales/nl/agent-chat.d.ts +47 -0
- package/dist/locales/nl/agent-chat.js +48 -1
- package/dist/prompts/index.d.ts +3 -5
- package/dist/prompts/index.js +3 -5
- package/dist/providers/index.d.ts +3 -9
- package/dist/providers/index.js +3 -9
- package/dist/state/index.d.ts +2 -2
- package/dist/state/index.js +2 -2
- package/dist/state/usage.d.ts +0 -1
- package/dist/state/usage.js +0 -3
- package/dist/theme.css +10 -0
- package/package.json +9 -8
- package/src/chat/index.ts +2 -23
- package/src/chat/logic/continue-after-stop.ts +32 -0
- package/src/chat/logic/thread-cards.ts +0 -5
- package/src/chat/preferences.ts +0 -4
- package/src/chat/stash.ts +17 -0
- package/src/chat/ui/Composer.tsx +33 -5
- package/src/chat/ui/Pickers.tsx +1 -0
- package/src/chat/ui/composer/ComposerControls.tsx +13 -3
- package/src/chat/ui/composer/use-composer-submit.ts +25 -5
- package/src/chat/ui/composer/use-continuable-turn.ts +14 -0
- package/src/chat/ui/composer/use-notice.ts +14 -7
- package/src/chat/ui/index.ts +9 -49
- package/src/chat/ui/intelligent-ui/UiReply.tsx +24 -1
- package/src/chat/ui/intelligent-ui/node-text.ts +34 -1
- package/src/chat/ui/intelligent-ui/registry.ts +33 -5
- package/src/chat/ui/intelligent-ui/render-context.ts +14 -0
- package/src/chat/ui/intelligent-ui/renderers/base-land.ts +7 -0
- package/src/chat/ui/intelligent-ui/renderers/binding.ts +14 -0
- package/src/chat/ui/intelligent-ui/renderers/content.tsx +144 -9
- package/src/chat/ui/intelligent-ui/renderers/inputs.tsx +79 -17
- package/src/chat/ui/intelligent-ui/renderers/map-slots.ts +105 -0
- package/src/chat/ui/intelligent-ui/renderers/map-style.ts +96 -0
- package/src/chat/ui/intelligent-ui/renderers/map-view.tsx +251 -0
- package/src/chat/ui/intelligent-ui/renderers/map.tsx +226 -0
- package/src/chat/ui/intelligent-ui/renderers/place-point.ts +16 -0
- package/src/chat/ui/intelligent-ui/renderers/text.tsx +28 -1
- package/src/chat/ui/intelligent-ui/use-in-view.ts +18 -0
- package/src/host.ts +28 -0
- package/src/index.ts +2 -12
- package/src/locales/en/agent-chat.ts +48 -1
- package/src/locales/nl/agent-chat.ts +48 -1
- package/src/prompts/index.ts +3 -18
- package/src/providers/index.ts +3 -9
- package/src/state/index.ts +1 -16
- package/src/state/usage.ts +0 -4
- package/src/stylesheets.d.ts +2 -0
- package/src/theme.css +10 -0
package/dist/providers/index.js
CHANGED
|
@@ -3,15 +3,9 @@ export { AccountDot } from '../agents/AccountDot.js';
|
|
|
3
3
|
export { AgentIcon } from '../agents/AgentIcon.js';
|
|
4
4
|
export { ModelOptionControl } from '../agents/ModelOptionControl.js';
|
|
5
5
|
export { ProviderLogo } from '../agents/ProviderLogo.js';
|
|
6
|
-
export { accountsOfKind,
|
|
6
|
+
export { accountsOfKind, ACCOUNT_TONE_CLASSES, accountStatusLine, canContinueOn } from '../agents/accounts.js';
|
|
7
7
|
export { modelName, useModelName, shortModelName } from '../agents/model-name.js';
|
|
8
|
-
export {
|
|
9
|
-
export {
|
|
10
|
-
export { AccountColors } from './AccountColors.js';
|
|
11
|
-
export { AccountDetail } from './AccountDetail.js';
|
|
12
|
-
export { AccountVariables } from './AccountVariables.js';
|
|
13
|
-
export { AddAccountForm } from './AddAccountForm.js';
|
|
14
|
-
export { CliDetail } from './CliDetail.js';
|
|
8
|
+
export { carryOptions } from '../agents/model-options.js';
|
|
9
|
+
export {} from '../agents/model-requirement.js';
|
|
15
10
|
export { ProvidersPane } from './ProvidersPane.js';
|
|
16
|
-
export { saveAccounts, saveAccount, removeAccount, createAccount, linkAccount } from './account-actions.js';
|
|
17
11
|
export { CliMark, CliTile } from './parts.js';
|
package/dist/state/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { type ChatState, type ChatsById, type ChatStatuses, type ChatSink,
|
|
1
|
+
export { type ChatState, type ChatsById, type ChatStatuses, type ChatSink, waitingRequestsOf, applyEvent, useChats, chatSink, useChatRow, type ChatHistoryPage, type ChatPage } from './chats.js';
|
|
2
2
|
export { useProviderAccountsStore, useProviderAccounts, providerAccountsOf, knownAccounts, watchProviderAccounts, type ProviderAccountsRow } from './provider-accounts.js';
|
|
3
3
|
export { useProvidersStore, useProviders, providersOf, providerSinkFor, type ProvidersRow } from './providers.js';
|
|
4
|
-
export { type UsageMetric, USAGE_PREFERENCES_KEY,
|
|
4
|
+
export { type UsageMetric, USAGE_PREFERENCES_KEY, dayOf, windowFor, reloadUsagePreferences, useUsageStore, type UsagePeriod, type UsageView } from './usage.js';
|
package/dist/state/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Public API: exports.test.ts snapshots this list, so a name added or dropped here is an API change.
|
|
2
|
-
export {
|
|
2
|
+
export { waitingRequestsOf, applyEvent, useChats, chatSink, useChatRow } from './chats.js';
|
|
3
3
|
export { useProviderAccountsStore, useProviderAccounts, providerAccountsOf, knownAccounts, watchProviderAccounts } from './provider-accounts.js';
|
|
4
4
|
export { useProvidersStore, useProviders, providersOf, providerSinkFor } from './providers.js';
|
|
5
|
-
export { USAGE_PREFERENCES_KEY,
|
|
5
|
+
export { USAGE_PREFERENCES_KEY, dayOf, windowFor, reloadUsagePreferences, useUsageStore } from './usage.js';
|
package/dist/state/usage.d.ts
CHANGED
package/dist/state/usage.js
CHANGED
|
@@ -14,9 +14,6 @@ export let USAGE_PREFERENCES_KEY = storageKey();
|
|
|
14
14
|
onChatStorageConfiguration(() => {
|
|
15
15
|
USAGE_PREFERENCES_KEY = storageKey();
|
|
16
16
|
});
|
|
17
|
-
export function usagePreferencesKey() {
|
|
18
|
-
return storageKey();
|
|
19
|
-
}
|
|
20
17
|
const DEFAULTS = { period: '7d', metric: 'cost', currency: 'USD' };
|
|
21
18
|
function parsePreferences(raw) {
|
|
22
19
|
try {
|
package/dist/theme.css
CHANGED
|
@@ -27,6 +27,9 @@
|
|
|
27
27
|
--chart-4: #eda100;
|
|
28
28
|
--chart-5: #e87ba4;
|
|
29
29
|
--chart-6: #008300;
|
|
30
|
+
/* The two grounds of the map a page draws without tiles. */
|
|
31
|
+
--map-land: #f2f1ed;
|
|
32
|
+
--map-water: #d3dde5;
|
|
30
33
|
}
|
|
31
34
|
|
|
32
35
|
[data-theme="dark"] {
|
|
@@ -46,6 +49,8 @@
|
|
|
46
49
|
--chart-4: #c98500;
|
|
47
50
|
--chart-5: #d55181;
|
|
48
51
|
--chart-6: #008300;
|
|
52
|
+
--map-land: #2c3135;
|
|
53
|
+
--map-water: #1a2026;
|
|
49
54
|
}
|
|
50
55
|
|
|
51
56
|
/* Output and diffs take the terminal's colors when the app loads `@adecore/terminal/terminal.css`,
|
|
@@ -56,6 +61,9 @@
|
|
|
56
61
|
--chat-added: var(--term-ansi-green, var(--positive));
|
|
57
62
|
--chat-removed: var(--term-ansi-red, var(--status-error));
|
|
58
63
|
--chart-1: var(--accent);
|
|
64
|
+
/* The number of a place on a map and in its list: the ink on the ground, so it reads on either. */
|
|
65
|
+
--map-marker: var(--text);
|
|
66
|
+
--map-marker-text: var(--surface);
|
|
59
67
|
}
|
|
60
68
|
|
|
61
69
|
@theme inline {
|
|
@@ -73,6 +81,8 @@
|
|
|
73
81
|
--color-chat-output: var(--chat-output);
|
|
74
82
|
--color-chat-added: var(--chat-added);
|
|
75
83
|
--color-chat-removed: var(--chat-removed);
|
|
84
|
+
--color-map-marker: var(--map-marker);
|
|
85
|
+
--color-map-marker-text: var(--map-marker-text);
|
|
76
86
|
}
|
|
77
87
|
|
|
78
88
|
@layer components {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adecore/agents-react",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.25.0",
|
|
4
4
|
"description": "React agent chat threads, composers, approvals, provider accounts and usage views.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -67,11 +67,11 @@
|
|
|
67
67
|
"test:visuals": "bun --config=integration.bunfig.toml --conditions=source test ./src/chat/visual-viewport.integration.test.ts"
|
|
68
68
|
},
|
|
69
69
|
"dependencies": {
|
|
70
|
-
"@adecore/agent-contracts": "0.
|
|
71
|
-
"@adecore/editor": "0.
|
|
72
|
-
"@adecore/editor-react": "0.
|
|
73
|
-
"@adecore/intelligent-ui": "0.
|
|
74
|
-
"@adecore/merge": "0.
|
|
70
|
+
"@adecore/agent-contracts": "0.25.0",
|
|
71
|
+
"@adecore/editor": "0.25.0",
|
|
72
|
+
"@adecore/editor-react": "0.25.0",
|
|
73
|
+
"@adecore/intelligent-ui": "0.25.0",
|
|
74
|
+
"@adecore/merge": "0.25.0",
|
|
75
75
|
"@codemirror/commands": "^6.11.1",
|
|
76
76
|
"@codemirror/language": "^6.12.4",
|
|
77
77
|
"@codemirror/state": "^6.7.6",
|
|
@@ -83,6 +83,7 @@
|
|
|
83
83
|
"@tanstack/react-virtual": "^3.14.13",
|
|
84
84
|
"clsx": "^2.1.1",
|
|
85
85
|
"lucide-react": "^1.47.0",
|
|
86
|
+
"maplibre-gl": "^6.14.0",
|
|
86
87
|
"mermaid": "^12.2.0",
|
|
87
88
|
"react-markdown": "^10.1.0",
|
|
88
89
|
"remark-breaks": "^4.0.0",
|
|
@@ -93,14 +94,14 @@
|
|
|
93
94
|
"zustand": "^5.0.15"
|
|
94
95
|
},
|
|
95
96
|
"peerDependencies": {
|
|
96
|
-
"@adecore/ui": "^0.
|
|
97
|
+
"@adecore/ui": "^0.25.0",
|
|
97
98
|
"i18next": "^26.4.2",
|
|
98
99
|
"react": "^19.3.0",
|
|
99
100
|
"react-dom": "^19.3.0",
|
|
100
101
|
"react-i18next": "^17.0.15"
|
|
101
102
|
},
|
|
102
103
|
"devDependencies": {
|
|
103
|
-
"@adecore/ui": "0.
|
|
104
|
+
"@adecore/ui": "0.25.0",
|
|
104
105
|
"@types/bun": "^1.4.2",
|
|
105
106
|
"@types/react": "^19.3.0",
|
|
106
107
|
"@types/react-dom": "^19.3.0",
|
package/src/chat/index.ts
CHANGED
|
@@ -1,18 +1,6 @@
|
|
|
1
1
|
// Public API: exports.test.ts snapshots this list, so a name added or dropped here is an API change.
|
|
2
2
|
export { type AccountChoice, useAccountChoice } from './account-choice';
|
|
3
3
|
export { transportActions, actionsOf, useChatActions } from './actions';
|
|
4
|
-
export { backgroundCounts } from './activity';
|
|
5
|
-
export {
|
|
6
|
-
isImageAttachment,
|
|
7
|
-
fileBadge,
|
|
8
|
-
formatBytes,
|
|
9
|
-
checkAttachmentLimits,
|
|
10
|
-
filesOf,
|
|
11
|
-
readAttachments,
|
|
12
|
-
readStoredAttachments,
|
|
13
|
-
uploadPreviewUrl,
|
|
14
|
-
uploadBytes
|
|
15
|
-
} from './attachments';
|
|
16
4
|
export { type ChatSendExtras, ChatClient } from './chat-client';
|
|
17
5
|
export type { ProjectChat } from './chat-references';
|
|
18
6
|
export { sendFromComposer, type ComposerSend } from './composer-send';
|
|
@@ -36,24 +24,17 @@ export {
|
|
|
36
24
|
export { handbackReportOf } from './logic/handback';
|
|
37
25
|
export { limitView, type LimitView } from './logic/limit';
|
|
38
26
|
export type { TickKind, ScrubberTick } from './logic/scrubber';
|
|
39
|
-
export { type TimedRow,
|
|
27
|
+
export { type TimedRow, withTimedRows, timedRowsOf } from './logic/thread-cards';
|
|
40
28
|
export { EMPTY_TARGET, readTimelineTarget, type TimelineTarget } from './logic/timeline-target';
|
|
41
29
|
export { type TimelineRow, summarizeGroup, deriveTimelineRows, type SubagentBranch, type BaseMoved } from './logic/timeline';
|
|
42
|
-
export { toolSummary,
|
|
30
|
+
export { toolSummary, type FileChange } from './logic/tools';
|
|
43
31
|
export {
|
|
44
32
|
MENTION_DRAG_TYPE,
|
|
45
33
|
LEGACY_MENTION_DRAG_TYPE,
|
|
46
|
-
MENTION_DRAG_TYPES,
|
|
47
34
|
carriesMentions,
|
|
48
35
|
droppedMentions,
|
|
49
36
|
writeMentionDrag,
|
|
50
|
-
findMentionQuery,
|
|
51
|
-
findSkillQuery,
|
|
52
|
-
insertMention,
|
|
53
|
-
insertSkill,
|
|
54
|
-
presentMentions,
|
|
55
37
|
chipText,
|
|
56
|
-
tokenizeChips,
|
|
57
38
|
type MentionQuery,
|
|
58
39
|
type ChipSegment
|
|
59
40
|
} from './mentions';
|
|
@@ -66,7 +47,6 @@ export {
|
|
|
66
47
|
useChatPreferences,
|
|
67
48
|
readChatPreferences,
|
|
68
49
|
CHAT_PREFERENCES_KEY,
|
|
69
|
-
chatPreferencesKey,
|
|
70
50
|
reloadChatPreferences,
|
|
71
51
|
rememberChatPreferences,
|
|
72
52
|
rememberReadingWidth,
|
|
@@ -93,6 +73,5 @@ export {
|
|
|
93
73
|
type SubagentStep,
|
|
94
74
|
type SubagentTrail
|
|
95
75
|
} from './subagent-view';
|
|
96
|
-
export { THUMBNAIL_PX, useUploadThumbnail } from './thumbnails';
|
|
97
76
|
export { stepTimelineMessage, jumpToTimelineItem, type TurnTarget, registerTurnJumper, jumpToTimelineTurn, type ItemJumpOptions } from './timeline-scroll';
|
|
98
77
|
export { type StatusWord, statusLookOf, taskStatusWord, type StatusLook, type TaskState } from '../agents/status-look';
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { abortedByEnd, abortedByMachine, type ChatInfo, type ChatItem } from '@adecore/agent-contracts';
|
|
2
|
+
|
|
3
|
+
/* English like the other prompts the CLI gets, and it reaches the thread as the person's own message, which it is. */
|
|
4
|
+
export const CONTINUE_PROMPT = 'Continue.';
|
|
5
|
+
|
|
6
|
+
/*
|
|
7
|
+
* The turn a person stopped last, when nothing has happened since and Continue can pick it up; null otherwise.
|
|
8
|
+
* A paused queue, a limit and a paused goal have their own way out, a turn the machine ended has its own resume,
|
|
9
|
+
* and a request that waits on a person comes first.
|
|
10
|
+
*/
|
|
11
|
+
export function continuableTurn(info: ChatInfo, items: readonly ChatItem[]): string | null {
|
|
12
|
+
if (
|
|
13
|
+
info.activeTurnId !== null ||
|
|
14
|
+
info.status === 'needs-you' ||
|
|
15
|
+
(info.queue ?? []).length > 0 ||
|
|
16
|
+
info.limit !== undefined ||
|
|
17
|
+
info.goal?.status === 'paused'
|
|
18
|
+
) {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
const index = items.findLastIndex((item) => item.kind === 'turn');
|
|
22
|
+
const turn = items[index];
|
|
23
|
+
if (turn?.kind !== 'turn' || turn.state !== 'aborted' || turn.limit !== undefined || abortedByMachine(turn, items) || abortedByEnd(turn, items)) {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
// A message of this turn sits after the turn item too, so only one sent after the turn ended counts.
|
|
27
|
+
const endedAt = turn.endedAt ?? turn.createdAt;
|
|
28
|
+
if (items.slice(index + 1).some((item) => item.kind === 'user' && item.createdAt > endedAt)) {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
return turn.id;
|
|
32
|
+
}
|
|
@@ -92,8 +92,3 @@ export function withTimedRows(rows: TimelineRow[], timed: readonly TimedRow[]):
|
|
|
92
92
|
export function timedRowsOf(cards: readonly ThreadCard[], visuals: readonly ChatVisual[] | null, heldFrom: number | null = null): TimedRow[] {
|
|
93
93
|
return [...(visuals === null ? [] : visualRows(visuals, heldFrom)), ...cardRows(cards)];
|
|
94
94
|
}
|
|
95
|
-
|
|
96
|
-
/* The thread's rows with an app's cards among them, the way `withTimedRows` places them. */
|
|
97
|
-
export function withThreadCards(rows: TimelineRow[], cards: readonly ThreadCard[]): TimelineRow[] {
|
|
98
|
-
return withTimedRows(rows, cardRows(cards));
|
|
99
|
-
}
|
package/src/chat/preferences.ts
CHANGED
|
@@ -143,10 +143,6 @@ onChatStorageConfiguration(() => {
|
|
|
143
143
|
CHAT_PREFERENCES_KEY = storageKey();
|
|
144
144
|
});
|
|
145
145
|
|
|
146
|
-
export function chatPreferencesKey(): string {
|
|
147
|
-
return storageKey();
|
|
148
|
-
}
|
|
149
|
-
|
|
150
146
|
/* Reads the remembered default again, once another window of the same origin wrote it. */
|
|
151
147
|
export function reloadChatPreferences(): void {
|
|
152
148
|
useChatPreferences.setState(storage.read(), true);
|
package/src/chat/stash.ts
CHANGED
|
@@ -26,6 +26,8 @@ export interface StashedPrompt {
|
|
|
26
26
|
text: string;
|
|
27
27
|
mentions: string[];
|
|
28
28
|
skills: string[];
|
|
29
|
+
/* Chats of the project picked with `@`; an entry from before this field has none. */
|
|
30
|
+
chats: string[];
|
|
29
31
|
attachments: StashedAttachment[];
|
|
30
32
|
/* A quote that knew its reply, kept apart from the text so it can lead back to it in the chat it came from. */
|
|
31
33
|
quote?: StashedQuote;
|
|
@@ -83,6 +85,7 @@ export function parseStash(raw: string | null): StashedPrompt[] {
|
|
|
83
85
|
text: entry.text as string,
|
|
84
86
|
mentions: strings(entry.mentions),
|
|
85
87
|
skills: strings(entry.skills),
|
|
88
|
+
chats: strings(entry.chats),
|
|
86
89
|
attachments: attachments(entry.attachments),
|
|
87
90
|
...stashedQuoteField(entry.quote),
|
|
88
91
|
createdAt: typeof entry.createdAt === 'number' ? entry.createdAt : 0
|
|
@@ -113,11 +116,25 @@ export function stashedFrom(draft: ChatDraft, id: string, now: number, chatId: s
|
|
|
113
116
|
...(kept ? { quote: { text: draft.quote, chatId, itemId: draft.quoteSource! } } : {}),
|
|
114
117
|
mentions: [...draft.mentions],
|
|
115
118
|
skills: [...draft.skills],
|
|
119
|
+
chats: [...draft.chats],
|
|
116
120
|
attachments: draft.attachments.map((attachment) => ({ name: attachment.name, mime: attachment.mime, size: uploadBytes(attachment) })),
|
|
117
121
|
createdAt: now
|
|
118
122
|
};
|
|
119
123
|
}
|
|
120
124
|
|
|
125
|
+
/* The limit of `chats` in a send. */
|
|
126
|
+
export const STASH_CHATS_LIMIT = 16;
|
|
127
|
+
|
|
128
|
+
/*
|
|
129
|
+
* The attached chats a composer may take back: the ones it can still reference, never its own chat, in the
|
|
130
|
+
* order they were picked. `dropped` counts the chats left out because the project no longer has them.
|
|
131
|
+
*/
|
|
132
|
+
export function restorableChats(stashed: readonly string[], referable: ReadonlySet<string>, chatId: string): { chats: string[]; dropped: number } {
|
|
133
|
+
const others = stashed.filter((id) => id !== chatId);
|
|
134
|
+
const chats = others.filter((id) => referable.has(id));
|
|
135
|
+
return { chats: chats.slice(0, STASH_CHATS_LIMIT), dropped: others.length - chats.length };
|
|
136
|
+
}
|
|
137
|
+
|
|
121
138
|
const storage = persistedJson<StashedPrompt[]>(storageKey, parseStash, [], () => chatStorageLegacyKeys('stash'));
|
|
122
139
|
|
|
123
140
|
/* Prompts put aside for later can move to another chat, so the shelf is shared across chats. */
|
package/src/chat/ui/Composer.tsx
CHANGED
|
@@ -36,6 +36,7 @@ import { useComposerDraft } from './composer/use-composer-draft';
|
|
|
36
36
|
import { useComposerMenus } from './composer/use-composer-menus';
|
|
37
37
|
import { useComposerSubmit } from './composer/use-composer-submit';
|
|
38
38
|
import { LEAVE_DROPS_TO_THE_CARD, useDraftFiles } from './composer/use-draft-files';
|
|
39
|
+
import { useContinuableTurn } from './composer/use-continuable-turn';
|
|
39
40
|
import { useNotice } from './composer/use-notice';
|
|
40
41
|
import { usePendingRequests } from './composer/use-pending-requests';
|
|
41
42
|
import { usePromptHistory } from './composer/use-prompt-history';
|
|
@@ -96,7 +97,11 @@ export function Composer({ chatId, info, focused, answerPromptsElsewhere, disabl
|
|
|
96
97
|
const files = useDraftFiles({ capabilities, providerName: provider?.name, writable, draft: box, inputRef, setNotice: notice.setNotice });
|
|
97
98
|
const controls = useChatControls({ chatId, info, notice, onRetarget });
|
|
98
99
|
const guard = promptGuard(withQuote(draft.quote, draft.text));
|
|
99
|
-
const
|
|
100
|
+
const continuable = useContinuableTurn(chatId, info);
|
|
101
|
+
// The stopped turn something went out for, since the chat takes a moment to answer and Continue must not stand there meanwhile.
|
|
102
|
+
const [sentFor, setSentFor] = useState<string | null>(null);
|
|
103
|
+
const extraPrompts = host.prompts.useExtra(scope.id, chatId);
|
|
104
|
+
const { submit, continueTurn, restoreStashed, toggleStash } = useComposerSubmit({
|
|
100
105
|
chatId,
|
|
101
106
|
writable,
|
|
102
107
|
guard,
|
|
@@ -109,7 +114,23 @@ export function Composer({ chatId, info, focused, answerPromptsElsewhere, disabl
|
|
|
109
114
|
hold,
|
|
110
115
|
inputRef,
|
|
111
116
|
setNotice: notice.setNotice,
|
|
112
|
-
onSend
|
|
117
|
+
onSend: (text, extras) => {
|
|
118
|
+
setSentFor(continuable);
|
|
119
|
+
const sent = onSend(text, extras);
|
|
120
|
+
// A send the host did not take leaves Continue where it was; the original promise still reaches its caller.
|
|
121
|
+
void Promise.resolve(sent).then(
|
|
122
|
+
(taken) => {
|
|
123
|
+
if (taken === false) {
|
|
124
|
+
setSentFor(null);
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
(error: unknown) => {
|
|
128
|
+
setSentFor(null);
|
|
129
|
+
throw error;
|
|
130
|
+
}
|
|
131
|
+
);
|
|
132
|
+
return sent;
|
|
133
|
+
}
|
|
113
134
|
});
|
|
114
135
|
const { takingBack, takeBack } = useTakeBack({
|
|
115
136
|
chatId,
|
|
@@ -231,7 +252,7 @@ export function Composer({ chatId, info, focused, answerPromptsElsewhere, disabl
|
|
|
231
252
|
quote={draft.quote}
|
|
232
253
|
source={draft.quoteSource}
|
|
233
254
|
onRemove={box.removeQuote}
|
|
234
|
-
onSourceGone={() => notice.setNotice(t('rows.user.quoteGone'))}
|
|
255
|
+
onSourceGone={() => notice.setNotice(t('rows.user.quoteGone'), 'info')}
|
|
235
256
|
/>
|
|
236
257
|
)}
|
|
237
258
|
{draft.chats.length > 0 && <DraftChats chats={draft.chats} onRemove={box.removeChat} />}
|
|
@@ -263,7 +284,10 @@ export function Composer({ chatId, info, focused, answerPromptsElsewhere, disabl
|
|
|
263
284
|
onPaste={files.onPaste}
|
|
264
285
|
/>
|
|
265
286
|
{notice.notice && (
|
|
266
|
-
<div
|
|
287
|
+
<div
|
|
288
|
+
className={clsx('px-5 pb-1 text-xs @max-md/composer:px-3.5', notice.tone === 'error' ? 'text-status-error' : 'text-text-muted')}
|
|
289
|
+
role={notice.tone === 'error' ? 'alert' : 'status'}
|
|
290
|
+
>
|
|
267
291
|
{notice.notice}
|
|
268
292
|
</div>
|
|
269
293
|
)}
|
|
@@ -290,7 +314,11 @@ export function Composer({ chatId, info, focused, answerPromptsElsewhere, disabl
|
|
|
290
314
|
disabled: empty || !writable || guard.tooLong,
|
|
291
315
|
label: t(sendLabelKey(busy, steerable, sendDelivery)),
|
|
292
316
|
kbd: sendShortcut(sendKey, draft.text.includes('\n')),
|
|
293
|
-
onSend: (alternate) => submit(alternate ? 'alternate' : 'default')
|
|
317
|
+
onSend: (alternate) => submit(alternate ? 'alternate' : 'default'),
|
|
318
|
+
onContinue:
|
|
319
|
+
continuable !== null && continuable !== sentFor && empty && writable && pending.length === 0 && extraPrompts.length === 0
|
|
320
|
+
? continueTurn
|
|
321
|
+
: null
|
|
294
322
|
}}
|
|
295
323
|
dictationToolbar={setDictationToolbar}
|
|
296
324
|
onFiles={(picked) => {
|
package/src/chat/ui/Pickers.tsx
CHANGED
|
@@ -273,6 +273,7 @@ export function StashPicker({ onRestore }: { onRestore(prompt: StashedPrompt): v
|
|
|
273
273
|
{prompt.attachments.length > 0 && (
|
|
274
274
|
<span className="text-text-faint">{t('pickers.stash.files', { count: prompt.attachments.length })}</span>
|
|
275
275
|
)}
|
|
276
|
+
{prompt.chats.length > 0 && <span className="text-text-faint">{t('pickers.stash.chats', { count: prompt.chats.length })}</span>}
|
|
276
277
|
</Popover.Close>
|
|
277
278
|
<IconButton
|
|
278
279
|
icon={Trash2}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { useRef, type Ref } from 'react';
|
|
2
2
|
import { useTranslation } from 'react-i18next';
|
|
3
|
-
import { ArrowUp, Plus, Square } from 'lucide-react';
|
|
3
|
+
import { ArrowUp, Play, Plus, Square } from 'lucide-react';
|
|
4
4
|
import type { ChatInfo } from '@adecore/agent-contracts';
|
|
5
|
-
import { ErrorBoundary, Icon, Tooltip, type Shortcut } from '@adecore/ui';
|
|
5
|
+
import { Button, ErrorBoundary, Icon, Tooltip, type Shortcut } from '@adecore/ui';
|
|
6
6
|
import { useChatHost } from '../../../host';
|
|
7
7
|
import { useChatScope } from '../../../scope';
|
|
8
8
|
import { useProviders } from '../../../state/providers';
|
|
@@ -20,6 +20,8 @@ interface SendProps {
|
|
|
20
20
|
label: string;
|
|
21
21
|
kbd: Shortcut;
|
|
22
22
|
onSend(alternate: boolean): void;
|
|
23
|
+
/* Set when a person stopped the last turn and the box is empty: Continue stands where the send button would. */
|
|
24
|
+
onContinue: (() => void) | null;
|
|
23
25
|
}
|
|
24
26
|
|
|
25
27
|
interface ComposerControlsProps {
|
|
@@ -130,7 +132,15 @@ export function ComposerControls({
|
|
|
130
132
|
</button>
|
|
131
133
|
</Tooltip>
|
|
132
134
|
)}
|
|
133
|
-
{send.
|
|
135
|
+
{send.onContinue !== null && (
|
|
136
|
+
<Tooltip label={t('composer.continueHint')}>
|
|
137
|
+
<Button variant="primary" shape="pill" disabled={controls.configurationPending} onClick={send.onContinue}>
|
|
138
|
+
<Icon icon={Play} size={14} />
|
|
139
|
+
{t('composer.continue')}
|
|
140
|
+
</Button>
|
|
141
|
+
</Tooltip>
|
|
142
|
+
)}
|
|
143
|
+
{send.onContinue === null && send.visible && (
|
|
134
144
|
<Tooltip label={send.label} kbd={send.kbd} name>
|
|
135
145
|
<button
|
|
136
146
|
className="flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-accent text-accent-text hover:brightness-90 disabled:opacity-50 disabled:hover:brightness-100"
|
|
@@ -1,17 +1,20 @@
|
|
|
1
1
|
import { useEffect, useRef, type RefObject } from 'react';
|
|
2
|
+
import { useTranslation } from 'react-i18next';
|
|
2
3
|
import { useChatHost } from '../../../host';
|
|
3
4
|
import { useChatScope } from '../../../scope';
|
|
4
5
|
import { checkAttachmentLimits, uploadBytes } from '../../attachments';
|
|
5
6
|
import type { ChatSendExtras } from '../../chat-client';
|
|
6
7
|
import { registerComposerSender, type ComposerSend } from '../../composer-send';
|
|
7
8
|
import { isEmptyDraft } from '../../drafts';
|
|
9
|
+
import { CONTINUE_PROMPT } from '../../logic/continue-after-stop';
|
|
8
10
|
import type { PromptGuard } from '../../guards';
|
|
9
11
|
import { presentMentions, presentSkills } from '../../mentions';
|
|
10
12
|
import { withQuote } from '../../quote';
|
|
11
|
-
import { stashDraft, useStash, type StashedPrompt } from '../../stash';
|
|
13
|
+
import { restorableChats, stashDraft, useStash, type StashedPrompt } from '../../stash';
|
|
12
14
|
import { scrollTimelineToEnd } from '../../timeline-scroll';
|
|
13
15
|
import type { ComposerInputHandle } from '../ComposerInput';
|
|
14
16
|
import type { ChatControls } from './use-chat-controls';
|
|
17
|
+
import type { NoticeTone } from './use-notice';
|
|
15
18
|
import type { ComposerDraft } from './use-composer-draft';
|
|
16
19
|
import type { ComposerMenus } from './use-composer-menus';
|
|
17
20
|
import type { PromptHistory } from './use-prompt-history';
|
|
@@ -31,7 +34,7 @@ interface SubmitInput {
|
|
|
31
34
|
history: PromptHistory;
|
|
32
35
|
hold: ComposerPromptHold;
|
|
33
36
|
inputRef: RefObject<ComposerInputHandle | null>;
|
|
34
|
-
setNotice(notice: string): void;
|
|
37
|
+
setNotice(notice: string, tone?: NoticeTone): void;
|
|
35
38
|
onSend(text: string, extras: ChatSendExtras): void | Promise<boolean>;
|
|
36
39
|
}
|
|
37
40
|
|
|
@@ -51,9 +54,11 @@ export function useComposerSubmit({
|
|
|
51
54
|
setNotice,
|
|
52
55
|
onSend
|
|
53
56
|
}: SubmitInput) {
|
|
57
|
+
const { t } = useTranslation('agent-chat');
|
|
54
58
|
const host = useChatHost();
|
|
55
59
|
const scope = useChatScope();
|
|
56
60
|
const { draft, setDraft } = box;
|
|
61
|
+
const referable = host.useReferableChats();
|
|
57
62
|
|
|
58
63
|
/* Opens the host's new chat once the host took the message; a host that answers nothing took it. */
|
|
59
64
|
const afterSend = (sent: void | Promise<boolean>, andNew: boolean): void => {
|
|
@@ -127,23 +132,38 @@ export function useComposerSubmit({
|
|
|
127
132
|
useEffect(() => registerComposerSender(scope.keyOf(chatId), (how) => submitRef.current(how)), [scope, chatId]);
|
|
128
133
|
|
|
129
134
|
/*
|
|
130
|
-
* A stashed prompt comes back as text, mentions and
|
|
131
|
-
* keeps leading to its reply in the chat it came from, and is plain
|
|
135
|
+
* A stashed prompt comes back as text, mentions, skills and the chats this project still has; its
|
|
136
|
+
* files were never kept. Its quote keeps leading to its reply in the chat it came from, and is plain
|
|
137
|
+
* text in any other.
|
|
132
138
|
*/
|
|
133
139
|
const restoreStashed = (prompt: StashedPrompt): void => {
|
|
134
140
|
const stashedQuote = prompt.quote;
|
|
135
141
|
const here = stashedQuote !== undefined && stashedQuote.chatId === chatId;
|
|
142
|
+
const { chats, dropped } = restorableChats(prompt.chats, new Set(referable.map((chat) => chat.id)), chatId);
|
|
136
143
|
setDraft((current) => ({
|
|
137
144
|
...current,
|
|
138
145
|
text: stashedQuote === undefined || here ? prompt.text : withQuote(stashedQuote.text, prompt.text),
|
|
139
146
|
mentions: [...prompt.mentions],
|
|
140
147
|
skills: [...prompt.skills],
|
|
148
|
+
chats,
|
|
141
149
|
...(here ? { quote: stashedQuote.text, quoteSource: stashedQuote.itemId } : {})
|
|
142
150
|
}));
|
|
151
|
+
if (dropped > 0) {
|
|
152
|
+
setNotice(t('composer.notice.stashChatsDropped', { count: dropped }), 'info');
|
|
153
|
+
}
|
|
143
154
|
history.forget();
|
|
144
155
|
inputRef.current?.focus();
|
|
145
156
|
};
|
|
146
157
|
|
|
158
|
+
/* Sends the prompt that goes on after a stop through the same door as a typed one, so the host sees a person's message. */
|
|
159
|
+
const continueTurn = (): void => {
|
|
160
|
+
if (!writable) {
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
scrollTimelineToEnd(chatId, 'instant');
|
|
164
|
+
afterSend(onSend(CONTINUE_PROMPT, {}), false);
|
|
165
|
+
};
|
|
166
|
+
|
|
147
167
|
/* Cmd+S puts the draft away and clears the box; on an empty box the same key takes the last one back. */
|
|
148
168
|
const toggleStash = (): void => {
|
|
149
169
|
if (isEmptyDraft(draft)) {
|
|
@@ -159,5 +179,5 @@ export function useComposerSubmit({
|
|
|
159
179
|
}
|
|
160
180
|
};
|
|
161
181
|
|
|
162
|
-
return { submit, restoreStashed, toggleStash };
|
|
182
|
+
return { submit, continueTurn, restoreStashed, toggleStash };
|
|
163
183
|
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { useMemo } from 'react';
|
|
2
|
+
import type { ChatInfo, ChatItem } from '@adecore/agent-contracts';
|
|
3
|
+
import { useChatRow } from '../../../state/chats';
|
|
4
|
+
import { continuableTurn } from '../../logic/continue-after-stop';
|
|
5
|
+
|
|
6
|
+
/* The turn a person stopped that Continue would pick up, or null when there is none. */
|
|
7
|
+
export function useContinuableTurn(chatId: string, info: ChatInfo): string | null {
|
|
8
|
+
const order = useChatRow(chatId, (row) => row?.order);
|
|
9
|
+
const structure = useChatRow(chatId, (row) => row?.structure);
|
|
10
|
+
return useMemo(() => {
|
|
11
|
+
const items = (order ?? []).map((id) => structure?.[id]).filter((item): item is ChatItem => item !== undefined);
|
|
12
|
+
return continuableTurn(info, items);
|
|
13
|
+
}, [info, order, structure]);
|
|
14
|
+
}
|
|
@@ -1,27 +1,34 @@
|
|
|
1
|
-
import { useEffect, useState } from 'react';
|
|
1
|
+
import { useCallback, useEffect, useState } from 'react';
|
|
2
2
|
|
|
3
3
|
const NOTICE_MS = 4000;
|
|
4
4
|
|
|
5
|
+
/* An error is what went wrong; info is something the composer did that the person should know. */
|
|
6
|
+
export type NoticeTone = 'error' | 'info';
|
|
7
|
+
|
|
5
8
|
export interface ComposerNotice {
|
|
6
9
|
notice: string | null;
|
|
7
|
-
|
|
10
|
+
tone: NoticeTone;
|
|
11
|
+
setNotice(notice: string | null, tone?: NoticeTone): void;
|
|
8
12
|
showError(error: unknown): void;
|
|
9
13
|
}
|
|
10
14
|
|
|
11
15
|
/* A line under the box that says what went wrong, gone again after a few seconds. */
|
|
12
16
|
export function useNotice(): ComposerNotice {
|
|
13
|
-
const [
|
|
17
|
+
const [shown, setShown] = useState<{ text: string; tone: NoticeTone } | null>(null);
|
|
14
18
|
|
|
15
19
|
useEffect(() => {
|
|
16
|
-
if (!
|
|
20
|
+
if (!shown) {
|
|
17
21
|
return;
|
|
18
22
|
}
|
|
19
|
-
const timer = setTimeout(() =>
|
|
23
|
+
const timer = setTimeout(() => setShown(null), NOTICE_MS);
|
|
20
24
|
return () => clearTimeout(timer);
|
|
21
|
-
}, [
|
|
25
|
+
}, [shown]);
|
|
26
|
+
|
|
27
|
+
const setNotice = useCallback((notice: string | null, tone: NoticeTone = 'error'): void => setShown(notice ? { text: notice, tone } : null), []);
|
|
22
28
|
|
|
23
29
|
return {
|
|
24
|
-
notice,
|
|
30
|
+
notice: shown?.text ?? null,
|
|
31
|
+
tone: shown?.tone ?? 'error',
|
|
25
32
|
setNotice,
|
|
26
33
|
showError: (error) => setNotice(error instanceof Error ? error.message : String(error))
|
|
27
34
|
};
|