@fungi.computer/parakeet 0.3.0 → 0.5.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 (63) hide show
  1. package/dist/account-commands.js +20 -7
  2. package/dist/client-contributions.d.ts +1 -0
  3. package/dist/commands/awaiting-driver.d.ts +20 -0
  4. package/dist/commands/awaiting-driver.js +121 -0
  5. package/dist/commands/builtins.js +6 -4
  6. package/dist/commands/interactions.d.ts +25 -0
  7. package/dist/commands/interactions.js +76 -2
  8. package/dist/commands/run.js +3 -3
  9. package/dist/commands/slash.d.ts +7 -3
  10. package/dist/commands/slash.js +2 -0
  11. package/dist/commands/use-slash-commands.d.ts +13 -0
  12. package/dist/commands/use-slash-commands.js +58 -1
  13. package/dist/components/ChatComposer.d.ts +2 -0
  14. package/dist/components/ChatComposer.js +150 -38
  15. package/dist/components/ClientRows.js +42 -26
  16. package/dist/components/CompletionMenu.d.ts +54 -0
  17. package/dist/components/CompletionMenu.js +52 -0
  18. package/dist/components/DecisionComposer.d.ts +30 -0
  19. package/dist/components/DecisionComposer.js +146 -0
  20. package/dist/components/InteractionPanel.d.ts +10 -1
  21. package/dist/components/InteractionPanel.js +24 -5
  22. package/dist/components/RunFailureNotice.js +11 -2
  23. package/dist/components/SlashMenu.d.ts +1 -3
  24. package/dist/components/SlashMenu.js +5 -13
  25. package/dist/components/TimelineItem.d.ts +1 -0
  26. package/dist/components/TimelineItem.js +3 -3
  27. package/dist/components/ToolGroup.d.ts +1 -0
  28. package/dist/components/ToolGroup.js +7 -7
  29. package/dist/conversation.d.ts +5 -1
  30. package/dist/conversation.js +72 -20
  31. package/dist/core/approval-decision.d.ts +198 -0
  32. package/dist/core/approval-decision.js +291 -0
  33. package/dist/core/awaiting-answer.d.ts +229 -0
  34. package/dist/core/awaiting-answer.js +303 -0
  35. package/dist/core/build-timeline.js +56 -5
  36. package/dist/core/client-actions.d.ts +1 -0
  37. package/dist/core/client-actions.js +8 -21
  38. package/dist/core/draft-storage.d.ts +4 -0
  39. package/dist/core/draft-storage.js +12 -5
  40. package/dist/core/picker-text.d.ts +7 -0
  41. package/dist/core/picker-text.js +24 -0
  42. package/dist/core/session-query.d.ts +8 -0
  43. package/dist/core/session-query.js +13 -11
  44. package/dist/core/session-view-state.d.ts +1 -1
  45. package/dist/core/session-view-state.js +14 -6
  46. package/dist/headless.d.ts +7 -2
  47. package/dist/headless.js +6 -1
  48. package/dist/model-commands.js +14 -15
  49. package/dist/parakeet.d.ts +6 -2
  50. package/dist/parakeet.js +15 -6
  51. package/dist/session-commands.js +6 -2
  52. package/dist/store/types.d.ts +5 -1
  53. package/dist/styles.css +407 -113
  54. package/dist/use-approval-awaiting.d.ts +22 -0
  55. package/dist/use-approval-awaiting.js +81 -0
  56. package/dist/use-decision.d.ts +20 -0
  57. package/dist/use-decision.js +79 -0
  58. package/docs/api/commands/slash.md +30 -26
  59. package/docs/api/headless.md +822 -41
  60. package/docs/api/index.md +13 -4
  61. package/package.json +15 -8
  62. package/src/completion-menu.css +71 -0
  63. package/src/styles.css +174 -46
@@ -15,6 +15,7 @@ const recalledPrompt = z.object({
15
15
  delivery,
16
16
  });
17
17
  const pendingDraft = z.object({
18
+ status: z.enum(["queued-local", "admitting"]),
18
19
  requestId: z.string().min(1),
19
20
  content: z.string(),
20
21
  submittedDraft: z.string(),
@@ -31,15 +32,21 @@ const journal = z.object({
31
32
  });
32
33
  /** One owner-scoped journal commits pending text and composer text together. */
33
34
  export function draftJournal(scope, storage) {
34
- const key = `fungi.parakeet.drafts.v1:${scope}`;
35
+ const key = `fungi.parakeet.drafts.v2:${scope}`;
35
36
  return {
36
37
  read() {
37
38
  if (storage === undefined)
38
39
  return { draft: "", pending: [] };
39
- const saved = storage.getItem(key);
40
- return saved === null
41
- ? { draft: "", pending: [] }
42
- : journal.parse(JSON.parse(saved));
40
+ try {
41
+ const saved = storage.getItem(key);
42
+ return saved === null
43
+ ? { draft: "", pending: [] }
44
+ : journal.parse(JSON.parse(saved));
45
+ }
46
+ catch {
47
+ // Preview journals are disposable; a broken load must not break chat.
48
+ return { draft: "", pending: [] };
49
+ }
43
50
  },
44
51
  write(value) {
45
52
  if (storage === undefined)
@@ -0,0 +1,7 @@
1
+ /**
2
+ * When something happened, as a picker column: `14:02` for today, `Yesterday`,
3
+ * or `2026-10-24`. Shared by every picker that lists past work.
4
+ */
5
+ export declare function formatWhen(epochMillis: number, now: number): string;
6
+ /** `text` on one line, whitespace collapsed and cut at a word to `limit`. */
7
+ export declare function oneLine(text: string, limit: number): string;
@@ -0,0 +1,24 @@
1
+ const pad = (value) => String(value).padStart(2, "0");
2
+ const dayStart = (date) => new Date(date.getFullYear(), date.getMonth(), date.getDate()).getTime();
3
+ /**
4
+ * When something happened, as a picker column: `14:02` for today, `Yesterday`,
5
+ * or `2026-10-24`. Shared by every picker that lists past work.
6
+ */
7
+ export function formatWhen(epochMillis, now) {
8
+ const at = new Date(epochMillis);
9
+ const days = Math.round((dayStart(new Date(now)) - dayStart(at)) / 86_400_000);
10
+ if (days <= 0)
11
+ return `${pad(at.getHours())}:${pad(at.getMinutes())}`;
12
+ if (days === 1)
13
+ return "Yesterday";
14
+ return `${String(at.getFullYear())}-${pad(at.getMonth() + 1)}-${pad(at.getDate())}`;
15
+ }
16
+ /** `text` on one line, whitespace collapsed and cut at a word to `limit`. */
17
+ export function oneLine(text, limit) {
18
+ const line = text.replace(/\s+/gu, " ").trim();
19
+ if (line.length <= limit)
20
+ return line;
21
+ const cut = line.slice(0, Math.max(0, limit - 1));
22
+ const space = cut.lastIndexOf(" ");
23
+ return `${(space > limit / 2 ? cut.slice(0, space) : cut).trimEnd()}…`;
24
+ }
@@ -7,6 +7,8 @@ export declare function sessionQueryOptions(session: ParakeetSession): import("@
7
7
  observation: "connecting" | "live";
8
8
  /** Last coherent durable Session state, absent before the first snapshot. */
9
9
  snapshot: SessionSnapshot | undefined;
10
+ /** Last successful watch connect or delivered frame, not a cached seed. */
11
+ lastReplyAt: number | undefined;
10
12
  /** One retryable user prompt, including image cargo, survives page refreshes. */
11
13
  latestPrompt: Readonly<{
12
14
  /** Owner-issued identity of the original prompt command. */
@@ -21,6 +23,8 @@ export declare function sessionQueryOptions(session: ParakeetSession): import("@
21
23
  observation: "connecting" | "live";
22
24
  /** Last coherent durable Session state, absent before the first snapshot. */
23
25
  snapshot: SessionSnapshot | undefined;
26
+ /** Last successful watch connect or delivered frame, not a cached seed. */
27
+ lastReplyAt: number | undefined;
24
28
  /** One retryable user prompt, including image cargo, survives page refreshes. */
25
29
  latestPrompt: Readonly<{
26
30
  /** Owner-issued identity of the original prompt command. */
@@ -36,6 +40,8 @@ export declare function sessionQueryOptions(session: ParakeetSession): import("@
36
40
  observation: "connecting" | "live";
37
41
  /** Last coherent durable Session state, absent before the first snapshot. */
38
42
  snapshot: SessionSnapshot | undefined;
43
+ /** Last successful watch connect or delivered frame, not a cached seed. */
44
+ lastReplyAt: number | undefined;
39
45
  /** One retryable user prompt, including image cargo, survives page refreshes. */
40
46
  latestPrompt: Readonly<{
41
47
  /** Owner-issued identity of the original prompt command. */
@@ -51,6 +57,8 @@ export declare function sessionQueryOptions(session: ParakeetSession): import("@
51
57
  observation: "connecting" | "live";
52
58
  /** Last coherent durable Session state, absent before the first snapshot. */
53
59
  snapshot: SessionSnapshot | undefined;
60
+ /** Last successful watch connect or delivered frame, not a cached seed. */
61
+ lastReplyAt: number | undefined;
54
62
  /** One retryable user prompt, including image cargo, survives page refreshes. */
55
63
  latestPrompt: Readonly<{
56
64
  /** Owner-issued identity of the original prompt command. */
@@ -4,6 +4,7 @@ import { reportFailure } from "../lib/failure.js";
4
4
  const emptyProjection = Object.freeze({
5
5
  observation: "connecting",
6
6
  snapshot: undefined,
7
+ lastReplyAt: undefined,
7
8
  latestPrompt: undefined,
8
9
  });
9
10
  // A watch starts with a bounded page, but its update frames keep arriving for
@@ -60,7 +61,7 @@ async function* observeSession(session, signal) {
60
61
  yield { kind: "snapshot", value: snapshot };
61
62
  }
62
63
  else {
63
- yield { kind: "frame", value: frame };
64
+ yield { kind: "frame", value: frame, snapshot };
64
65
  }
65
66
  }
66
67
  throw new Error("Session watch ended; current activity is unavailable");
@@ -81,21 +82,21 @@ function reduceObservation(projection, observation) {
81
82
  return {
82
83
  observation: "live",
83
84
  snapshot: observation.value,
85
+ lastReplyAt: Date.now(),
84
86
  latestPrompt: latestPrompt(observation.value.messages, projection.latestPrompt),
85
87
  };
86
- if (projection.snapshot === undefined)
87
- return observation.value.kind === "reset"
88
- ? {
89
- observation: "live",
90
- snapshot: observation.value.baseline,
91
- latestPrompt: latestPrompt(observation.value.baseline.messages),
92
- }
93
- : projection;
94
- const snapshot = applySessionFrame(projection.snapshot, observation.value);
88
+ // The generator owns frame application and always delivers its baseline
89
+ // first. The cache only reduces already-projected observations.
90
+ const snapshot = observation.snapshot;
95
91
  return {
96
92
  observation: "live",
97
93
  snapshot,
98
- latestPrompt: latestPrompt(snapshot.messages, projection.latestPrompt),
94
+ lastReplyAt: Date.now(),
95
+ latestPrompt: observation.value.kind === "reset" ||
96
+ (observation.value.kind === "update" &&
97
+ observation.value.messages.some((message) => message.value.role === "user"))
98
+ ? latestPrompt(snapshot.messages, projection.latestPrompt)
99
+ : projection.latestPrompt,
99
100
  };
100
101
  }
101
102
  /** One shared TanStack observation keyed by Shiitake's logical Session address. */
@@ -119,6 +120,7 @@ export function seedSessionQuery(queryClient, session, snapshot) {
119
120
  queryClient.setQueryData(sessionQueryOptions(session).queryKey, {
120
121
  observation: "connecting",
121
122
  snapshot,
123
+ lastReplyAt: undefined,
122
124
  latestPrompt: latestPrompt(snapshot.messages),
123
125
  });
124
126
  }
@@ -46,7 +46,7 @@ export type SessionViewState = Readonly<{
46
46
  }> | Readonly<{
47
47
  /** The latest terminal failed or was interrupted. */
48
48
  kind: "error";
49
- /** Bounded terminal diagnostic suitable for presentation. */
49
+ /** Redacted, bounded terminal diagnostic for presentation. */
50
50
  cause: string;
51
51
  }>;
52
52
  /** The facts a view state is decided from. */
@@ -1,8 +1,15 @@
1
+ import { boundedDiagnostic } from "@fungi.computer/bird-dog/effect";
2
+ import stripAnsi from "strip-ansi";
1
3
  const retryCauseLimit = 160;
2
- const errorCauseLimit = 200;
3
4
  function collapsed(text, limit) {
4
5
  return text.replace(/\s+/gu, " ").trim().slice(0, limit);
5
6
  }
7
+ function displayDiagnostic(text) {
8
+ // Recognize whole terminal programs before dropping individual controls:
9
+ // otherwise OSC payloads become visible text after their ESC is removed.
10
+ const safe = stripAnsi(text).replace(/[\p{Cc}\u200b\u200e\u200f\u061c\u202a-\u202e\u2066-\u2069\u2060\ufeff]/gu, (character) => (/\s/u.test(character) ? " " : ""));
11
+ return boundedDiagnostic(safe);
12
+ }
6
13
  function idleViewState({ activity, terminals, }) {
7
14
  let terminal;
8
15
  for (const candidate of terminals)
@@ -15,14 +22,15 @@ function idleViewState({ activity, terminals, }) {
15
22
  if (activity.lastModelChoiceSequence !== null &&
16
23
  activity.lastModelChoiceSequence > terminal.sequence)
17
24
  return { kind: "waiting_for_you" };
18
- if (terminal.outcome === "failed" || terminal.outcome === "interrupted")
25
+ if (terminal.outcome === "failed" || terminal.outcome === "interrupted") {
26
+ const fallback = terminal.outcome === "interrupted"
27
+ ? "Run was interrupted"
28
+ : "Run failed without a diagnostic";
19
29
  return {
20
30
  kind: "error",
21
- cause: collapsed(terminal.diagnostic ??
22
- (terminal.outcome === "interrupted"
23
- ? "Run was interrupted"
24
- : "Run failed without a diagnostic"), errorCauseLimit),
31
+ cause: displayDiagnostic(terminal.diagnostic ?? fallback) ?? fallback,
25
32
  };
33
+ }
26
34
  return { kind: "waiting_for_you" };
27
35
  }
28
36
  /** Decide a Session's view state from its activity, terminals and progress. */
@@ -2,8 +2,9 @@ export { formatModelIdentity, formatModelWithLevel, } from "./core/model-identit
2
2
  export type { QueuedRecallState, QueuedRecallOutcome, } from "./core/queued-recall.js";
3
3
  export type { ParakeetDraftIdentity, ParakeetDraftStorage, } from "./core/draft-storage.js";
4
4
  export { formatContextUsage, formatTokenCount } from "./core/format-context.js";
5
+ export { formatWhen, oneLine } from "./core/picker-text.js";
5
6
  export { providerChoices } from "./provider-choices.js";
6
- export { ParakeetProvider, useComposer, useClientContributions, useParakeetInteractions, useExtensionSlots, useParakeetSession, useStatus, useTimeline, type ComposerController, type ParakeetDraftCheckpoint, type ParakeetModel, type ParakeetObservation, type ParakeetReconnectOutcome, type ParakeetStatus, type ParakeetSessionViewState, type ParakeetUsage, type StatusController, type TimelineController, } from "./conversation.js";
7
+ export { ParakeetProvider, useComposer, useClientContributions, useParakeetInteractions, useExtensionSlots, useParakeetSession, useLastSessionReplyAt, useStatus, useTimeline, type ComposerController, type ParakeetDraftCheckpoint, type ParakeetModel, type ParakeetObservation, type ParakeetReconnectOutcome, type ParakeetStatus, type ParakeetSessionViewState, type ParakeetUsage, type StatusController, type TimelineController, } from "./conversation.js";
7
8
  export { sessionViewState, queuedSessionLabel, sessionActivityStatus, type SessionActivityStatus, type SessionViewFacts, type SessionViewState as ParakeetOwnerViewState, } from "./core/session-view-state.js";
8
9
  export { mergeTerminals } from "./core/session-projection.js";
9
10
  export { type SessionActivity, seedSessionQuery, sessionQueryOptions, subscribeSessionActivity, } from "./core/session-query.js";
@@ -14,7 +15,7 @@ export { contributeSessionCommands, type ParakeetSessionCommandOptions, } from "
14
15
  export { completeSlash, fuzzyMatch, keyBindings, keyLabel, parseSlashDraft, slashRegistry, type FuzzyMatch, type KeyBindingRow, type SlashCommand, type SlashCommandItem, type SlashCompletion, type SlashParse, type SlashWordItem, } from "./commands/slash.js";
15
16
  export { composerHints, fitHints, formatHints, type ComposerHintContext, type ComposerKeys, type KeyHint, } from "./commands/hints.js";
16
17
  export { isCancelledOutcome, runSlashDraft, type SlashRuntime, } from "./commands/run.js";
17
- export { createParakeetInteractions, type ParakeetInteractions, type ParakeetInteractionState, type ParakeetNotice, type ParakeetOpenInteraction, type ParakeetToast, } from "./commands/interactions.js";
18
+ export { createParakeetInteractions, type ParakeetInteractions, type ParakeetAwaiting, type ParakeetInteractionState, type ParakeetNotice, type ParakeetOpenInteraction, type ParakeetToast, } from "./commands/interactions.js";
18
19
  export { parakeetBuiltinCommands, type ParakeetBuiltinOptions, } from "./commands/builtins.js";
19
20
  export { advertisedCommands, type AdvertisedCatalogError, type AdvertisedCommands, } from "./commands/advertised.js";
20
21
  export { useSlashCommands, type ParakeetCommandRuntime, type SlashCommandController, type SlashCommandOptions, } from "./commands/use-slash-commands.js";
@@ -26,5 +27,9 @@ export { formatToolDuration, toolCallView, toolKindSigils, toolStatusTone, type
26
27
  export { assistantMessageVisible } from "./core/assistant-visibility.js";
27
28
  export { disclosureOpen, initialToolDisclosure, toggleAllDisclosure, toggleDisclosure, toolCallDefaultOpen, toolGroupDefaultOpen, visibleToolCalls, type ToolDisclosureState, } from "./core/tool-groups.js";
28
29
  export { useClientActions } from "./client-contributions.js";
30
+ export { awaitingQueue, awaitingTakeover, decisionView, decisionViewText, initialDecisionState, reduceDecision, type AwaitingItem, type AwaitingKind, type AwaitingQueue, type DecisionEffect, type DecisionIntent, type DecisionOutcome, type DecisionPhase, type DecisionPrompt, type DecisionRowView, type DecisionState, type DecisionView, } from "./core/awaiting-answer.js";
31
+ export { useDecision, type DecisionController } from "./use-decision.js";
32
+ export { useApprovalAwaiting } from "./use-approval-awaiting.js";
33
+ export { approvalCardCopy, approvalRefusal, askApproval, type ApprovalAbandoned, type ApprovalAnswered, type ApprovalAnswerInput, type ApprovalAnswerResult, type ApprovalCard, type ApprovalContent, type ApprovalDecision, type ApprovalEntry, type ApprovalFacts, type ApprovalOutcome, type ApprovalRefused, type ApprovalReview, type ApprovalSource, } from "./core/approval-decision.js";
29
34
  export { placeClientTimeline, type PlacedClientCell, } from "./core/client-timeline.js";
30
35
  export type { ClientContribution } from "./core/client-contributions.js";
package/dist/headless.js CHANGED
@@ -1,7 +1,8 @@
1
1
  export { formatModelIdentity, formatModelWithLevel, } from "./core/model-identity.js";
2
2
  export { formatContextUsage, formatTokenCount } from "./core/format-context.js";
3
+ export { formatWhen, oneLine } from "./core/picker-text.js";
3
4
  export { providerChoices } from "./provider-choices.js";
4
- export { ParakeetProvider, useComposer, useClientContributions, useParakeetInteractions, useExtensionSlots, useParakeetSession, useStatus, useTimeline, } from "./conversation.js";
5
+ export { ParakeetProvider, useComposer, useClientContributions, useParakeetInteractions, useExtensionSlots, useParakeetSession, useLastSessionReplyAt, useStatus, useTimeline, } from "./conversation.js";
5
6
  export { sessionViewState, queuedSessionLabel, sessionActivityStatus, } from "./core/session-view-state.js";
6
7
  export { mergeTerminals } from "./core/session-projection.js";
7
8
  export { seedSessionQuery, sessionQueryOptions, subscribeSessionActivity, } from "./core/session-query.js";
@@ -21,4 +22,8 @@ export { formatToolDuration, toolCallView, toolKindSigils, toolStatusTone, } fro
21
22
  export { assistantMessageVisible } from "./core/assistant-visibility.js";
22
23
  export { disclosureOpen, initialToolDisclosure, toggleAllDisclosure, toggleDisclosure, toolCallDefaultOpen, toolGroupDefaultOpen, visibleToolCalls, } from "./core/tool-groups.js";
23
24
  export { useClientActions } from "./client-contributions.js";
25
+ export { awaitingQueue, awaitingTakeover, decisionView, decisionViewText, initialDecisionState, reduceDecision, } from "./core/awaiting-answer.js";
26
+ export { useDecision } from "./use-decision.js";
27
+ export { useApprovalAwaiting } from "./use-approval-awaiting.js";
28
+ export { approvalCardCopy, approvalRefusal, askApproval, } from "./core/approval-decision.js";
24
29
  export { placeClientTimeline, } from "./core/client-timeline.js";
@@ -8,9 +8,6 @@ const defaultChoice = "default";
8
8
  const recentModels = new WeakMap();
9
9
  /** Private memory ceiling, independent of how many models a provider offers. */
10
10
  const maximumRememberedProviders = 256;
11
- function modelLabel(model) {
12
- return `${model.providerName} · ${model.name}`;
13
- }
14
11
  /** Each provider's current or last-used model first, then catalog order. */
15
12
  function orderModels(models, current, recent) {
16
13
  return [...models].sort((left, right) => {
@@ -33,19 +30,18 @@ async function chooseReasoning(interaction, model, current) {
33
30
  const answer = await interaction.prompt({
34
31
  type: "choice",
35
32
  message: `Thinking level for ${model.name}`,
33
+ summary: "How long the model thinks before it answers.",
34
+ current: current ?? defaultChoice,
36
35
  options: [
37
36
  { value: defaultChoice, label: "Model default" },
38
- ...model.reasoning.map((level) => ({
39
- value: level,
40
- label: level === current ? `${level} (current)` : level,
41
- })),
37
+ ...model.reasoning.map((level) => ({ value: level, label: level })),
42
38
  ],
43
39
  });
44
40
  return model.reasoning.find((level) => level === answer);
45
41
  }
46
42
  /** Leave one Whistle option for inheriting the Agent model. */
47
43
  const maximumModelOptions = 255;
48
- async function chooseProviderModels(interaction, models, account, refreshModels) {
44
+ async function chooseProviderModels(interaction, models, account, refreshModels, currentProviderId) {
49
45
  const { owner } = account;
50
46
  const [access, registry] = await Promise.all([
51
47
  owner.access(interaction.signal),
@@ -76,7 +72,9 @@ async function chooseProviderModels(interaction, models, account, refreshModels)
76
72
  }
77
73
  const providerId = await interaction.prompt({
78
74
  type: "choice",
79
- message: "Model",
75
+ message: "Choose a provider",
76
+ summary: "Or follow the Agent's own model.",
77
+ current: currentProviderId ?? defaultChoice,
80
78
  collapsedGroup: "More providers",
81
79
  options: [
82
80
  { value: defaultChoice, label: "Use agent model" },
@@ -156,21 +154,22 @@ export function sessionModelCommandDefinitions(session, options) {
156
154
  return session.modelPreference.read();
157
155
  const current = await session.modelPreference.read();
158
156
  remember(current);
159
- const providerModels = await chooseProviderModels(interaction, options.models, options.account, options.refreshModels);
157
+ const providerModels = await chooseProviderModels(interaction, options.models, options.account, options.refreshModels, current?.providerId);
160
158
  if (providerModels === null) {
161
159
  await interaction.run(() => session.modelPreference.set(null));
162
160
  return interaction.run(() => session.modelPreference.read());
163
161
  }
164
162
  const ordered = orderModels(providerModels, current, recent);
163
+ const currentIndex = ordered.findIndex((model) => model.providerId === current?.providerId &&
164
+ model.id === current.modelId);
165
165
  const answer = await interaction.prompt({
166
166
  type: "choice",
167
- message: "Model",
167
+ message: "Choose a model",
168
+ summary: `Models from ${ordered[0]?.providerName ?? "this provider"}.`,
169
+ current: currentIndex < 0 ? undefined : String(currentIndex),
168
170
  options: ordered.map((model, index) => ({
169
171
  value: String(index),
170
- label: model.providerId === current?.providerId &&
171
- model.id === current.modelId
172
- ? `${modelLabel(model)} (current)`
173
- : modelLabel(model),
172
+ label: model.name,
174
173
  })),
175
174
  });
176
175
  const model = ordered[Number(answer)];
@@ -31,6 +31,8 @@ export type ParakeetProps = Readonly<{
31
31
  composerActionVariant?: "primary" | "outline" | undefined;
32
32
  /** Optional caller-owned actions for a completed tool result. */
33
33
  renderToolActions?: ((item: ToolItem) => ReactNode) | undefined;
34
+ /** Optional host claim: true hides this item's image parts from the tool body because the host shows them. */
35
+ claimsToolMedia?: ((item: ToolItem) => boolean) | undefined;
34
36
  /** Optional caller-owned attachment below a settled assistant reply. */
35
37
  renderAssistantAttachment?: ((text: string) => ReactNode) | undefined;
36
38
  /** Optional display text; attachments still receive the original reply. */
@@ -54,6 +56,8 @@ export type ParakeetViewProps = Readonly<{
54
56
  composerActionVariant?: "primary" | "outline" | undefined;
55
57
  /** Optional caller-owned actions for a completed tool result. */
56
58
  renderToolActions?: ((item: ToolItem) => ReactNode) | undefined;
59
+ /** Optional host claim: true hides this item's image parts from the tool body because the host shows them. */
60
+ claimsToolMedia?: ((item: ToolItem) => boolean) | undefined;
57
61
  /** Optional caller-owned attachment below a settled assistant reply. */
58
62
  renderAssistantAttachment?: ((text: string) => ReactNode) | undefined;
59
63
  /** Optional display text; attachments still receive the original reply. */
@@ -72,6 +76,6 @@ export type ParakeetViewProps = Readonly<{
72
76
  commands?: ParakeetCommandRuntime | undefined;
73
77
  }>;
74
78
  /** Render the retained conversation surface within one Parakeet provider. */
75
- export declare function ParakeetView({ disambiguateAuthors, visitor, composerActionVariant, empty, commands, renderToolActions, renderAssistantAttachment, formatAssistantText, onLink, onOpen, }: ParakeetViewProps): ReactElement;
79
+ export declare function ParakeetView({ disambiguateAuthors, visitor, composerActionVariant, empty, commands, renderToolActions, claimsToolMedia, renderAssistantAttachment, formatAssistantText, onLink, onOpen, }: ParakeetViewProps): ReactElement;
76
80
  /** Standalone typed composer and exact-message timeline. */
77
- export declare function Parakeet({ authorOf, disambiguateAuthors, visitor, markdownPolicy, composerActionVariant, empty, session, clients, renderToolActions, renderAssistantAttachment, formatAssistantText, commands, onLink, onOpen, }: ParakeetProps): ReactElement;
81
+ export declare function Parakeet({ authorOf, disambiguateAuthors, visitor, markdownPolicy, composerActionVariant, empty, session, clients, renderToolActions, claimsToolMedia, renderAssistantAttachment, formatAssistantText, commands, onLink, onOpen, }: ParakeetProps): ReactElement;
package/dist/parakeet.js CHANGED
@@ -6,6 +6,7 @@ import { createWhistle } from "@fungi.computer/whistle";
6
6
  import { useVirtualizer } from "@tanstack/react-virtual";
7
7
  import { useCallback, useEffect, useMemo, useRef, useState, } from "react";
8
8
  import { ParakeetProvider, useComposer, useClientContributions, useDismissFailedPrompt, useStatus, useTimeline, } from "./conversation.js";
9
+ import { awaitingTakeover } from "./core/awaiting-answer.js";
9
10
  import { initialToolDisclosure, toggleDisclosure } from "./core/tool-groups.js";
10
11
  import { authorDisplayNames, } from "./core/author-labels.js";
11
12
  import { useTimelineItemKey } from "./core/use-timeline-item-key.js";
@@ -19,7 +20,7 @@ import { useSlashCommands, } from "./commands/use-slash-commands.js";
19
20
  import { TimelineItem } from "./components/TimelineItem.js";
20
21
  import { MarkdownPolicyContext } from "./components/MarkdownRenderer.js";
21
22
  /** Render the retained conversation surface within one Parakeet provider. */
22
- export function ParakeetView({ disambiguateAuthors, visitor, composerActionVariant, empty, commands, renderToolActions, renderAssistantAttachment, formatAssistantText, onLink, onOpen, }) {
23
+ export function ParakeetView({ disambiguateAuthors, visitor, composerActionVariant, empty, commands, renderToolActions, claimsToolMedia, renderAssistantAttachment, formatAssistantText, onLink, onOpen, }) {
23
24
  const textareaRef = useRef(null);
24
25
  const [ownCommands] = useState(() => createWhistle());
25
26
  const slash = useSlashCommands({
@@ -31,6 +32,9 @@ export function ParakeetView({ disambiguateAuthors, visitor, composerActionVaria
31
32
  });
32
33
  const clientActions = useClientActions(commands ?? ownCommands);
33
34
  const contributions = useClientContributions();
35
+ const { draft } = useComposer();
36
+ // The decision in the composer is not also a footer row.
37
+ const answering = awaitingTakeover(slash.awaiting, draft)?.key;
34
38
  const timelineController = useTimeline();
35
39
  const placed = useMemo(() => placeClientTimeline(contributions, timelineController.commandAnchors, timelineController.positionAnchors), [
36
40
  contributions,
@@ -43,7 +47,11 @@ export function ParakeetView({ disambiguateAuthors, visitor, composerActionVaria
43
47
  : null;
44
48
  if (anchor instanceof HTMLAnchorElement && onLink?.(anchor.href))
45
49
  event.preventDefault();
46
- }, children: [_jsxs("div", { className: "flex h-full w-full flex-col border-r border-base-300", children: [_jsx(ConversationTimeline, { disambiguateAuthors: visitor === undefined && disambiguateAuthors === true, clientActions: clientActions, onOpen: onOpen, focusAfterJump: textareaRef, empty: empty, renderToolActions: renderToolActions, renderAssistantAttachment: renderAssistantAttachment, formatAssistantText: formatAssistantText }), contributions.some(({ value }) => value.panel.length > 0) ? (_jsx("section", { "aria-label": "Session extension controls", className: "w-full shrink-0 border-t border-base-300 bg-base-100", children: contributions.map(({ entryId, value }) => {
50
+ }, children: [_jsxs("div", { className: "flex h-full w-full flex-col border-r border-base-300", children: [_jsx(ConversationTimeline, { disambiguateAuthors: visitor === undefined && disambiguateAuthors === true, clientActions: clientActions, onOpen: onOpen, focusAfterJump: textareaRef, empty: empty, renderToolActions: renderToolActions, claimsToolMedia: claimsToolMedia, renderAssistantAttachment: renderAssistantAttachment, formatAssistantText: formatAssistantText }), contributions.some(({ entryId, value }) => value.panel.some((row) => JSON.stringify([entryId, row.id]) !== answering)) ? (_jsx("section", { "aria-label": "Session extension controls", className: "w-full shrink-0 border-t border-base-300 bg-base-100", children: contributions.map(({ entryId, value: contribution }) => {
51
+ const value = {
52
+ ...contribution,
53
+ panel: contribution.panel.filter((row) => JSON.stringify([entryId, row.id]) !== answering),
54
+ };
47
55
  // An extension's retained-event notice and null-anchor rows
48
56
  // join its own footer entry; one with no panel row here keeps
49
57
  // none of this, rather than growing a section just to show it.
@@ -92,7 +100,7 @@ function ConversationInteraction({ slash, }) {
92
100
  slash.store.close();
93
101
  }, children: _jsx(DialogContent, { className: "w-[min(32rem,calc(100vw-2rem))] rounded-box bg-base-100 p-md shadow-lg", keepMounted: false, children: open === undefined ? null : (_jsx(InteractionPanel, { host: open.host, onClose: slash.store.close }, open.key)) }) }));
94
102
  }
95
- function ConversationTimeline({ disambiguateAuthors, clientActions, onOpen, focusAfterJump, empty, renderToolActions, renderAssistantAttachment, formatAssistantText, }) {
103
+ function ConversationTimeline({ disambiguateAuthors, clientActions, onOpen, focusAfterJump, empty, renderToolActions, claimsToolMedia, renderAssistantAttachment, formatAssistantText, }) {
96
104
  const dismissFailed = useDismissFailedPrompt();
97
105
  const timelineController = useTimeline();
98
106
  const cancelQueued = useCallback((commandId) => {
@@ -209,7 +217,7 @@ function ConversationTimeline({ disambiguateAuthors, clientActions, onOpen, focu
209
217
  item.author.name,
210
218
  },
211
219
  }
212
- : item, onDismissFailed: dismissFailed, onCancelQueued: cancelQueued, cancellingQueued: timelineController.cancellingQueued, onToggleTool: toggleTool, renderToolActions: renderToolActions, renderAssistantAttachment: renderAssistantAttachment, formatAssistantText: formatAssistantText, toolDisclosure: toolDisclosure }), placed.after.get(item.id)?.map(({ entryId, cell }) => (_jsx(ClientRows, { entryId: entryId, rows: cell.rows, actions: clientActions, onOpen: onOpen }, `${entryId}:${cell.id}`)))] }, virtualItem.key));
220
+ : item, onDismissFailed: dismissFailed, onCancelQueued: cancelQueued, cancellingQueued: timelineController.cancellingQueued, onToggleTool: toggleTool, renderToolActions: renderToolActions, claimsToolMedia: claimsToolMedia, renderAssistantAttachment: renderAssistantAttachment, formatAssistantText: formatAssistantText, toolDisclosure: toolDisclosure }), placed.after.get(item.id)?.map(({ entryId, cell }) => (_jsx(ClientRows, { entryId: entryId, rows: cell.rows, actions: clientActions, onOpen: onOpen }, `${entryId}:${cell.id}`)))] }, virtualItem.key));
213
221
  }) })) : before.length > 0 ? (before) : hasMoreHistory ? null : ((empty ?? (_jsx("div", { className: "flex h-full items-center justify-center text-center", children: _jsxs("div", { children: [_jsx("p", { className: "mb-1 text-xs text-base-content", children: "Ready" }), _jsx("p", { className: "text-[10px] text-dim", children: "What would you like to build?" })] }) })))) }), count > 0 ? (_jsx(ScrollToBottomBlob, { direction: "forward", unread: unread, focusAfterJump: focusAfterJump, onJump: jumpToBottom, scrollerRef: scrollerRef })) : null] }));
214
222
  }
215
223
  /** The owner's queued state and independently contributed waiting controls. */
@@ -242,6 +250,7 @@ function ConversationComposer({ visitor, textareaRef, composerActionVariant, sla
242
250
  }, [composer.cancel]);
243
251
  const composerProps = {
244
252
  queuedRecall: visitor === undefined ? composer : undefined,
253
+ onImagesChange: visitor === undefined ? composer.setImages : undefined,
245
254
  statusChips: contributions
246
255
  .flatMap(({ value }) => value.chips)
247
256
  .sort((left, right) => left.order - right.order)
@@ -270,6 +279,6 @@ function ConversationComposer({ visitor, textareaRef, composerActionVariant, sla
270
279
  return _jsx(ChatComposer, { ...composerProps });
271
280
  }
272
281
  /** Standalone typed composer and exact-message timeline. */
273
- export function Parakeet({ authorOf, disambiguateAuthors, visitor, markdownPolicy = "session", composerActionVariant, empty, session, clients, renderToolActions, renderAssistantAttachment, formatAssistantText, commands, onLink, onOpen, }) {
274
- return (_jsx(MarkdownPolicyContext.Provider, { value: markdownPolicy, children: _jsx(ParakeetProvider, { session: session, clients: clients, authorOf: authorOf, children: _jsx(ParakeetView, { disambiguateAuthors: disambiguateAuthors, visitor: visitor, composerActionVariant: composerActionVariant, empty: empty, renderToolActions: renderToolActions, renderAssistantAttachment: renderAssistantAttachment, formatAssistantText: formatAssistantText, commands: commands, onLink: onLink, onOpen: onOpen }) }) }));
282
+ export function Parakeet({ authorOf, disambiguateAuthors, visitor, markdownPolicy = "session", composerActionVariant, empty, session, clients, renderToolActions, claimsToolMedia, renderAssistantAttachment, formatAssistantText, commands, onLink, onOpen, }) {
283
+ return (_jsx(MarkdownPolicyContext.Provider, { value: markdownPolicy, children: _jsx(ParakeetProvider, { session: session, clients: clients, authorOf: authorOf, children: _jsx(ParakeetView, { disambiguateAuthors: disambiguateAuthors, visitor: visitor, composerActionVariant: composerActionVariant, empty: empty, renderToolActions: renderToolActions, claimsToolMedia: claimsToolMedia, renderAssistantAttachment: renderAssistantAttachment, formatAssistantText: formatAssistantText, commands: commands, onLink: onLink, onOpen: onOpen }) }) }));
275
284
  }
@@ -2,6 +2,7 @@ import { nanoid } from "@fungi.computer/cairn";
2
2
  import { ShiitakeError } from "@fungi.computer/shiitake";
3
3
  import { z } from "zod";
4
4
  import { buildDurableTimeline } from "./core/build-timeline.js";
5
+ import { formatWhen, oneLine } from "./core/picker-text.js";
5
6
  import { sessionActivityStatus, sessionViewState, } from "./core/session-view-state.js";
6
7
  /** A choice prompt holds at most 256 options; the default read already bounds this. */
7
8
  const maxForkOptions = 200;
@@ -132,12 +133,15 @@ export function contributeSessionCommands(commands, session, options) {
132
133
  if (interaction === undefined)
133
134
  throw new Error("Forking a Session needs a person.");
134
135
  const ordered = [...candidates].reverse().slice(0, maxForkOptions);
136
+ const now = Date.now();
135
137
  const answer = await interaction.prompt({
136
138
  type: "choice",
137
- message: "Fork from message",
139
+ message: "Fork from which message?",
140
+ summary: "Start a new conversation from your message.",
138
141
  options: ordered.map((item) => ({
139
142
  value: item.id,
140
- label: item.text.slice(0, 100) || "(empty message)",
143
+ label: oneLine(item.text, 44) || "(empty message)",
144
+ description: item.at === undefined ? undefined : formatWhen(item.at, now),
141
145
  })),
142
146
  });
143
147
  const forked = await interaction.run(async () => {
@@ -51,7 +51,7 @@ export type LocalPromptAdmission = Readonly<{
51
51
  /** Existing Session delivery intent. */
52
52
  delivery: PromptDelivery;
53
53
  /** Current local admission result. */
54
- status: "admitting" | "accepted" | "failed";
54
+ status: "queued-local" | "admitting" | "accepted" | "failed";
55
55
  /** User-presentable admission error when status is failed. */
56
56
  error?: string;
57
57
  }>;
@@ -73,6 +73,10 @@ export type AssistantMessageItem = Readonly<{
73
73
  text: string;
74
74
  /** Pi's private failed-turn error text; never a terminal or delegation diagnostic. */
75
75
  providerError?: string;
76
+ /** Short assistant output retained before a provider failure, not provider prose. */
77
+ errorDetail?: string;
78
+ /** Consecutive failures with the same visible headline and detail. */
79
+ repeatCount?: number;
76
80
  /** Assistant reasoning text, when supplied by the model. */
77
81
  thinking?: string;
78
82
  /** Usage carried by the exact assistant message. */