@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.
Files changed (103) hide show
  1. package/dist/chat/index.d.ts +4 -7
  2. package/dist/chat/index.js +4 -7
  3. package/dist/chat/logic/continue-after-stop.d.ts +3 -0
  4. package/dist/chat/logic/continue-after-stop.js +28 -0
  5. package/dist/chat/logic/thread-cards.d.ts +0 -1
  6. package/dist/chat/logic/thread-cards.js +0 -4
  7. package/dist/chat/preferences.d.ts +0 -1
  8. package/dist/chat/preferences.js +0 -3
  9. package/dist/chat/stash.d.ts +6 -0
  10. package/dist/chat/stash.js +13 -0
  11. package/dist/chat/ui/Composer.js +26 -5
  12. package/dist/chat/ui/Pickers.js +1 -1
  13. package/dist/chat/ui/composer/ComposerControls.d.ts +1 -0
  14. package/dist/chat/ui/composer/ComposerControls.js +3 -3
  15. package/dist/chat/ui/composer/use-composer-submit.d.ts +3 -1
  16. package/dist/chat/ui/composer/use-composer-submit.js +22 -4
  17. package/dist/chat/ui/composer/use-continuable-turn.d.ts +2 -0
  18. package/dist/chat/ui/composer/use-continuable-turn.js +12 -0
  19. package/dist/chat/ui/composer/use-notice.d.ts +3 -1
  20. package/dist/chat/ui/composer/use-notice.js +8 -6
  21. package/dist/chat/ui/index.d.ts +9 -28
  22. package/dist/chat/ui/index.js +8 -27
  23. package/dist/chat/ui/intelligent-ui/UiReply.js +20 -2
  24. package/dist/chat/ui/intelligent-ui/node-text.d.ts +3 -1
  25. package/dist/chat/ui/intelligent-ui/node-text.js +32 -0
  26. package/dist/chat/ui/intelligent-ui/registry.js +15 -5
  27. package/dist/chat/ui/intelligent-ui/render-context.d.ts +6 -0
  28. package/dist/chat/ui/intelligent-ui/renderers/base-land.d.ts +1 -0
  29. package/dist/chat/ui/intelligent-ui/renderers/base-land.js +6 -0
  30. package/dist/chat/ui/intelligent-ui/renderers/binding.d.ts +6 -0
  31. package/dist/chat/ui/intelligent-ui/renderers/binding.js +10 -0
  32. package/dist/chat/ui/intelligent-ui/renderers/content.d.ts +2 -0
  33. package/dist/chat/ui/intelligent-ui/renderers/content.js +68 -6
  34. package/dist/chat/ui/intelligent-ui/renderers/inputs.d.ts +2 -0
  35. package/dist/chat/ui/intelligent-ui/renderers/inputs.js +40 -13
  36. package/dist/chat/ui/intelligent-ui/renderers/map-slots.d.ts +20 -0
  37. package/dist/chat/ui/intelligent-ui/renderers/map-slots.js +87 -0
  38. package/dist/chat/ui/intelligent-ui/renderers/map-style.d.ts +18 -0
  39. package/dist/chat/ui/intelligent-ui/renderers/map-style.js +72 -0
  40. package/dist/chat/ui/intelligent-ui/renderers/map-view.d.ts +15 -0
  41. package/dist/chat/ui/intelligent-ui/renderers/map-view.js +213 -0
  42. package/dist/chat/ui/intelligent-ui/renderers/map.d.ts +3 -0
  43. package/dist/chat/ui/intelligent-ui/renderers/map.js +113 -0
  44. package/dist/chat/ui/intelligent-ui/renderers/place-point.d.ts +3 -0
  45. package/dist/chat/ui/intelligent-ui/renderers/place-point.js +13 -0
  46. package/dist/chat/ui/intelligent-ui/renderers/text.d.ts +2 -0
  47. package/dist/chat/ui/intelligent-ui/renderers/text.js +14 -1
  48. package/dist/chat/ui/intelligent-ui/use-in-view.d.ts +1 -0
  49. package/dist/chat/ui/intelligent-ui/use-in-view.js +17 -0
  50. package/dist/host.d.ts +16 -0
  51. package/dist/index.d.ts +2 -2
  52. package/dist/index.js +1 -1
  53. package/dist/locales/en/agent-chat.d.ts +47 -0
  54. package/dist/locales/en/agent-chat.js +48 -1
  55. package/dist/locales/nl/agent-chat.d.ts +47 -0
  56. package/dist/locales/nl/agent-chat.js +48 -1
  57. package/dist/prompts/index.d.ts +3 -5
  58. package/dist/prompts/index.js +3 -5
  59. package/dist/providers/index.d.ts +3 -9
  60. package/dist/providers/index.js +3 -9
  61. package/dist/state/index.d.ts +2 -2
  62. package/dist/state/index.js +2 -2
  63. package/dist/state/usage.d.ts +0 -1
  64. package/dist/state/usage.js +0 -3
  65. package/dist/theme.css +10 -0
  66. package/package.json +9 -8
  67. package/src/chat/index.ts +2 -23
  68. package/src/chat/logic/continue-after-stop.ts +32 -0
  69. package/src/chat/logic/thread-cards.ts +0 -5
  70. package/src/chat/preferences.ts +0 -4
  71. package/src/chat/stash.ts +17 -0
  72. package/src/chat/ui/Composer.tsx +33 -5
  73. package/src/chat/ui/Pickers.tsx +1 -0
  74. package/src/chat/ui/composer/ComposerControls.tsx +13 -3
  75. package/src/chat/ui/composer/use-composer-submit.ts +25 -5
  76. package/src/chat/ui/composer/use-continuable-turn.ts +14 -0
  77. package/src/chat/ui/composer/use-notice.ts +14 -7
  78. package/src/chat/ui/index.ts +9 -49
  79. package/src/chat/ui/intelligent-ui/UiReply.tsx +24 -1
  80. package/src/chat/ui/intelligent-ui/node-text.ts +34 -1
  81. package/src/chat/ui/intelligent-ui/registry.ts +33 -5
  82. package/src/chat/ui/intelligent-ui/render-context.ts +14 -0
  83. package/src/chat/ui/intelligent-ui/renderers/base-land.ts +7 -0
  84. package/src/chat/ui/intelligent-ui/renderers/binding.ts +14 -0
  85. package/src/chat/ui/intelligent-ui/renderers/content.tsx +144 -9
  86. package/src/chat/ui/intelligent-ui/renderers/inputs.tsx +79 -17
  87. package/src/chat/ui/intelligent-ui/renderers/map-slots.ts +105 -0
  88. package/src/chat/ui/intelligent-ui/renderers/map-style.ts +96 -0
  89. package/src/chat/ui/intelligent-ui/renderers/map-view.tsx +251 -0
  90. package/src/chat/ui/intelligent-ui/renderers/map.tsx +226 -0
  91. package/src/chat/ui/intelligent-ui/renderers/place-point.ts +16 -0
  92. package/src/chat/ui/intelligent-ui/renderers/text.tsx +28 -1
  93. package/src/chat/ui/intelligent-ui/use-in-view.ts +18 -0
  94. package/src/host.ts +28 -0
  95. package/src/index.ts +2 -12
  96. package/src/locales/en/agent-chat.ts +48 -1
  97. package/src/locales/nl/agent-chat.ts +48 -1
  98. package/src/prompts/index.ts +3 -18
  99. package/src/providers/index.ts +3 -9
  100. package/src/state/index.ts +1 -16
  101. package/src/state/usage.ts +0 -4
  102. package/src/stylesheets.d.ts +2 -0
  103. package/src/theme.css +10 -0
@@ -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, accountName, ACCOUNT_TONE_CLASSES, accountStatusLine, canContinueOn } from '../agents/accounts.js';
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 { optionValue, carryOptions } from '../agents/model-options.js';
9
- export { modelRequirementOf } from '../agents/model-requirement.js';
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';
@@ -1,4 +1,4 @@
1
- export { type ChatState, type ChatsById, type ChatStatuses, type ChatSink, prependPage, waitingRequestsOf, applyEvent, useChats, chatSink, useChatRow, useReportedModels, useCurrentItem, type ChatHistoryPage, type ChatPage } from './chats.js';
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, usagePreferencesKey, dayOf, windowFor, summaryPayload, reloadUsagePreferences, useUsageStore, useUsage, type UsagePeriod, type UsageView } from './usage.js';
4
+ export { type UsageMetric, USAGE_PREFERENCES_KEY, dayOf, windowFor, reloadUsagePreferences, useUsageStore, type UsagePeriod, type UsageView } from './usage.js';
@@ -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 { prependPage, waitingRequestsOf, applyEvent, useChats, chatSink, useChatRow, useReportedModels, useCurrentItem } from './chats.js';
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, usagePreferencesKey, dayOf, windowFor, summaryPayload, reloadUsagePreferences, useUsageStore, useUsage } from './usage.js';
5
+ export { USAGE_PREFERENCES_KEY, dayOf, windowFor, reloadUsagePreferences, useUsageStore } from './usage.js';
@@ -7,7 +7,6 @@ export declare const USAGE_PERIODS: readonly {
7
7
  days: number;
8
8
  }[];
9
9
  export declare let USAGE_PREFERENCES_KEY: string;
10
- export declare function usagePreferencesKey(): string;
11
10
  interface Preferences {
12
11
  period: UsagePeriod;
13
12
  metric: UsageMetric;
@@ -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.24.2",
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.24.2",
71
- "@adecore/editor": "0.24.2",
72
- "@adecore/editor-react": "0.24.2",
73
- "@adecore/intelligent-ui": "0.24.2",
74
- "@adecore/merge": "0.24.2",
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.24.2",
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.24.2",
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, cardRows, visualRows, withTimedRows, timedRowsOf, withThreadCards } from './logic/thread-cards';
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, fileChanges, unifiedChanges, approvalChanges, hasFileChanges, type FileChange } from './logic/tools';
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
- }
@@ -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. */
@@ -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 { submit, restoreStashed, toggleStash } = useComposerSubmit({
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 className="px-5 pb-1 text-xs text-status-error @max-md/composer:px-3.5" role="alert">
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) => {
@@ -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.visible && (
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 skills; its files were never kept. Its quote
131
- * keeps leading to its reply in the chat it came from, and is plain text in any other.
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
- setNotice(notice: string | null): void;
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 [notice, setNotice] = useState<string | null>(null);
17
+ const [shown, setShown] = useState<{ text: string; tone: NoticeTone } | null>(null);
14
18
 
15
19
  useEffect(() => {
16
- if (!notice) {
20
+ if (!shown) {
17
21
  return;
18
22
  }
19
- const timer = setTimeout(() => setNotice(null), NOTICE_MS);
23
+ const timer = setTimeout(() => setShown(null), NOTICE_MS);
20
24
  return () => clearTimeout(timer);
21
- }, [notice]);
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
  };