@deepagents/context 0.37.1 → 0.38.1

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/index.js CHANGED
@@ -199,6 +199,12 @@ function assistant(message2) {
199
199
  }
200
200
  };
201
201
  }
202
+ function toMessageFragment(item) {
203
+ if (isFragment(item) && isMessageFragment(item)) {
204
+ return item;
205
+ }
206
+ return message(item);
207
+ }
202
208
  function message(content) {
203
209
  const message2 = typeof content === "string" ? {
204
210
  id: generateId(),
@@ -1313,7 +1319,7 @@ function reminder(textOrFragment, options) {
1313
1319
  if (typeof text === "string") {
1314
1320
  assertReminderText(text);
1315
1321
  }
1316
- const asPart = options?.asPart ?? fromFragment2;
1322
+ const asPart = options?.asPart ?? false;
1317
1323
  if (options && "when" in options && options.when) {
1318
1324
  return {
1319
1325
  name: "reminder",
@@ -1869,7 +1875,6 @@ import {
1869
1875
  NoSuchToolError as NoSuchToolError2,
1870
1876
  ToolCallRepairError,
1871
1877
  createUIMessageStream as createUIMessageStream2,
1872
- generateId as generateId4,
1873
1878
  isToolUIPart
1874
1879
  } from "ai";
1875
1880
 
@@ -1877,7 +1882,10 @@ import {
1877
1882
  import { z as z2 } from "zod";
1878
1883
 
1879
1884
  // packages/context/src/lib/engine.ts
1880
- import { validateUIMessages } from "ai";
1885
+ import {
1886
+ generateId as generateId4,
1887
+ validateUIMessages
1888
+ } from "ai";
1881
1889
  import { mergeWith } from "lodash-es";
1882
1890
 
1883
1891
  // packages/context/src/lib/estimate.ts
@@ -2585,6 +2593,9 @@ function estimateMessageContent(data) {
2585
2593
  function isLanguageModelUsage(value) {
2586
2594
  return typeof value === "object" && value !== null && "totalTokens" in value;
2587
2595
  }
2596
+ function isEmptyAssistantPlaceholder(message2) {
2597
+ return message2.role === "assistant" && message2.parts.length === 0;
2598
+ }
2588
2599
  var ContextEngine = class _ContextEngine {
2589
2600
  /** Non-message fragments (role, hints, etc.) - not persisted in graph */
2590
2601
  #fragments = [];
@@ -2766,6 +2777,120 @@ var ContextEngine = class _ContextEngine {
2766
2777
  const { turn } = await this.#getChainContext();
2767
2778
  return turn;
2768
2779
  }
2780
+ async firstUserMessage() {
2781
+ await this.#ensureInitialized();
2782
+ if (this.#branch?.headMessageId) {
2783
+ const chain = await this.#store.getMessageChain(
2784
+ this.#branch.headMessageId
2785
+ );
2786
+ for (const msg of chain) {
2787
+ if (msg.name === "user") return msg.data;
2788
+ }
2789
+ }
2790
+ for (const fragment2 of this.#pendingMessages) {
2791
+ if (fragment2.name !== "user") continue;
2792
+ if (!fragment2.codec) {
2793
+ throw new Error(`Fragment "${fragment2.name}" is missing codec.`);
2794
+ }
2795
+ return fragment2.codec.encode();
2796
+ }
2797
+ return void 0;
2798
+ }
2799
+ /**
2800
+ * Return the head of the conversation — pending tail or persisted branch head.
2801
+ *
2802
+ * Includes empty assistant placeholders (use this for id-lookup, not for
2803
+ * building model prompts — see `getMessages()` for prompt-ready output).
2804
+ *
2805
+ * @throws if the pending tail is missing an id (programming error).
2806
+ */
2807
+ async headMessage() {
2808
+ await this.#ensureInitialized();
2809
+ if (this.#pendingMessages.length > 0) {
2810
+ const tail = this.#pendingMessages[this.#pendingMessages.length - 1];
2811
+ if (!tail.id) {
2812
+ throw new Error(
2813
+ `headMessage: pending fragment "${tail.name}" is missing id`
2814
+ );
2815
+ }
2816
+ return { id: tail.id, name: tail.name };
2817
+ }
2818
+ if (this.#branch?.headMessageId) {
2819
+ const msg = await this.#store.getMessage(this.#branch.headMessageId);
2820
+ if (msg) return { id: msg.id, name: msg.name };
2821
+ }
2822
+ return void 0;
2823
+ }
2824
+ /**
2825
+ * Return the model-ready conversation: persisted chain plus pending fragments,
2826
+ * with empty assistant placeholders filtered out.
2827
+ *
2828
+ * For id-lookup use `headMessage()` instead — that one keeps placeholders.
2829
+ */
2830
+ async getMessages() {
2831
+ await this.#ensureInitialized();
2832
+ const messages = [];
2833
+ if (this.#branch?.headMessageId) {
2834
+ const chain = await this.#store.getMessageChain(
2835
+ this.#branch.headMessageId
2836
+ );
2837
+ for (const msg of chain) {
2838
+ const data = msg.data;
2839
+ if (isEmptyAssistantPlaceholder(data)) continue;
2840
+ messages.push(data);
2841
+ }
2842
+ }
2843
+ for (const fragment2 of this.#pendingMessages) {
2844
+ if (!fragment2.codec) {
2845
+ throw new Error(`Fragment "${fragment2.name}" is missing codec.`);
2846
+ }
2847
+ const encoded = fragment2.codec.encode();
2848
+ if (isEmptyAssistantPlaceholder(encoded)) continue;
2849
+ messages.push(encoded);
2850
+ }
2851
+ return messages.length === 0 ? [] : validateUIMessages({ messages });
2852
+ }
2853
+ /**
2854
+ * Advance the conversation by one turn. Required setup before `chat()`.
2855
+ *
2856
+ * - User input → persists the message AND appends an empty assistant
2857
+ * placeholder reserving the id of the next streamed response.
2858
+ * - Assistant input (tool-resume / continuation) → persists in-place
2859
+ * (`branch: false`), reusing the input's id.
2860
+ *
2861
+ * Always leaves the chain head as an assistant fragment, satisfying chat()'s
2862
+ * precondition.
2863
+ *
2864
+ * @returns the assistant id that will receive the streamed response — useful
2865
+ * for telemetry, optimistic UI, or correlating logs before the stream starts.
2866
+ * @throws if assistant input is missing an id.
2867
+ *
2868
+ * @example
2869
+ * ```ts
2870
+ * const assistantId = await context.continue(user('hi'));
2871
+ * const stream = await chat(agent); // streams into assistantId
2872
+ * ```
2873
+ */
2874
+ async continue(input) {
2875
+ const fragment2 = toMessageFragment(input);
2876
+ const isAssistantUpdate = fragment2.name === "assistant";
2877
+ let assistantId;
2878
+ if (isAssistantUpdate) {
2879
+ if (!fragment2.id) {
2880
+ throw new Error("continue: assistant input is missing id");
2881
+ }
2882
+ assistantId = fragment2.id;
2883
+ this.set(fragment2);
2884
+ } else {
2885
+ assistantId = generateId4();
2886
+ this.set(
2887
+ fragment2,
2888
+ assistant({ id: assistantId, role: "assistant", parts: [] })
2889
+ );
2890
+ }
2891
+ await this.save({ branch: !isAssistantUpdate });
2892
+ return assistantId;
2893
+ }
2769
2894
  /**
2770
2895
  * Add fragments to the context.
2771
2896
  *
@@ -2812,25 +2937,8 @@ var ContextEngine = class _ContextEngine {
2812
2937
  async resolve(options) {
2813
2938
  await this.#ensureInitialized();
2814
2939
  const systemPrompt = options.renderer.render(this.#renderableFragments);
2815
- const messages = [];
2816
- if (this.#branch?.headMessageId) {
2817
- const chain = await this.#store.getMessageChain(
2818
- this.#branch.headMessageId
2819
- );
2820
- for (const msg of chain) {
2821
- messages.push(msg.data);
2822
- }
2823
- }
2824
- for (const fragment2 of this.#pendingMessages) {
2825
- if (!fragment2.codec) {
2826
- throw new Error(`Fragment "${fragment2.name}" is missing codec.`);
2827
- }
2828
- messages.push(fragment2.codec.encode());
2829
- }
2830
- return {
2831
- systemPrompt,
2832
- messages: messages.length === 0 ? [] : await validateUIMessages({ messages })
2833
- };
2940
+ const messages = await this.getMessages();
2941
+ return { systemPrompt, messages };
2834
2942
  }
2835
2943
  /**
2836
2944
  * Save pending messages to the graph.
@@ -2893,6 +3001,7 @@ var ContextEngine = class _ContextEngine {
2893
3001
  content: plainText,
2894
3002
  lastMessageAt,
2895
3003
  lastMessage,
3004
+ currentMessage: original,
2896
3005
  chat: this.#chatData,
2897
3006
  usage,
2898
3007
  branch: this.#branchName,
@@ -3745,55 +3854,75 @@ Examples:
3745
3854
  - "hi" -> New Conversation
3746
3855
  - "debug my python code" -> Python Debugging`;
3747
3856
  var titleSchema = z2.object({ title: z2.string() });
3748
- function extractText(message2) {
3749
- const cleaned = stripReminders(message2);
3750
- const textPart = cleaned.parts.find((p) => p.type === "text");
3751
- return textPart && "text" in textPart ? textPart.text : "";
3752
- }
3753
- function truncateTitle(text) {
3754
- if (!text) return "New Chat";
3755
- return text.length > 100 ? text.slice(0, 100) + "..." : text;
3756
- }
3757
- function staticChatTitle(message2) {
3758
- return truncateTitle(extractText(message2));
3759
- }
3760
- async function generateChatTitle(options) {
3761
- const text = extractText(options.message);
3762
- const fallback = truncateTitle(text);
3763
- if (!text) return fallback;
3764
- const store2 = new InMemoryContextStore();
3765
- const context = new ContextEngine({
3766
- store: store2,
3767
- chatId: crypto.randomUUID(),
3768
- userId: "system"
3769
- });
3770
- context.set(role(TITLE_PROMPT), user(text));
3771
- try {
3857
+ var TitleGenerator = class {
3858
+ #context;
3859
+ constructor(options) {
3860
+ this.#context = options.context;
3861
+ }
3862
+ async ensure(options) {
3863
+ const msg = await this.#firstUntitledUser();
3864
+ if (!msg) return null;
3865
+ try {
3866
+ const title = await this.#generateTitle(msg, options);
3867
+ return this.#applyTitle(title, "llm");
3868
+ } catch (error) {
3869
+ console.warn(
3870
+ "TitleGenerator: LLM title generation failed, falling back to static.",
3871
+ error
3872
+ );
3873
+ return this.#applyTitle(this.#staticTitle(msg), "static");
3874
+ }
3875
+ }
3876
+ async ensureStatic() {
3877
+ const msg = await this.#firstUntitledUser();
3878
+ if (!msg) return null;
3879
+ return this.#applyTitle(this.#staticTitle(msg), "static");
3880
+ }
3881
+ #staticTitle(message2) {
3882
+ return this.#truncateTitle(this.#extractText(message2));
3883
+ }
3884
+ async #generateTitle(message2, options) {
3885
+ const text = this.#extractText(message2);
3886
+ if (!text) {
3887
+ throw new Error(
3888
+ "Cannot generate chat title: message has no text content."
3889
+ );
3890
+ }
3891
+ const store2 = new InMemoryContextStore();
3892
+ const context = new ContextEngine({
3893
+ store: store2,
3894
+ chatId: crypto.randomUUID(),
3895
+ userId: "system"
3896
+ });
3897
+ context.set(role(TITLE_PROMPT), user(text));
3772
3898
  const { title } = await structuredOutput({
3773
3899
  context,
3774
3900
  model: options.model,
3775
3901
  schema: titleSchema
3776
3902
  }).generate({}, { abortSignal: options.abortSignal });
3777
- return title || fallback;
3778
- } catch (error) {
3779
- console.warn("Title generation failed, using fallback:", error);
3780
- return fallback;
3903
+ if (!title) {
3904
+ throw new Error("Title generation returned an empty string.");
3905
+ }
3906
+ return title;
3781
3907
  }
3782
- }
3783
-
3784
- // packages/context/src/lib/chat.ts
3785
- function toMessageFragment(item) {
3786
- if (isFragment(item) && isMessageFragment(item)) {
3787
- return item;
3908
+ #extractText(message2) {
3909
+ return extractPlainText(stripReminders(message2));
3788
3910
  }
3789
- return message(item);
3790
- }
3791
- function chatMessageToUIMessage(item) {
3792
- if (isFragment(item) && isMessageFragment(item)) {
3793
- return item.codec.encode();
3911
+ #truncateTitle(text) {
3912
+ if (!text) return "New Chat";
3913
+ return text.length > 100 ? text.slice(0, 100) + "..." : text;
3794
3914
  }
3795
- return item;
3796
- }
3915
+ async #applyTitle(title, source) {
3916
+ await this.#context.updateChat({ title });
3917
+ return { title, source };
3918
+ }
3919
+ async #firstUntitledUser() {
3920
+ if (this.#context.chat?.title) return void 0;
3921
+ return this.#context.firstUserMessage();
3922
+ }
3923
+ };
3924
+
3925
+ // packages/context/src/lib/chat.ts
3797
3926
  var defaultChatMessageMetadata = ({
3798
3927
  part
3799
3928
  }) => {
@@ -3805,7 +3934,7 @@ var defaultChatMessageMetadata = ({
3805
3934
  }
3806
3935
  return void 0;
3807
3936
  };
3808
- async function chat(agent2, messages, options) {
3937
+ async function chat(agent2, options = {}) {
3809
3938
  const context = agent2.context;
3810
3939
  const sandbox = agent2.sandbox;
3811
3940
  if (!context) {
@@ -3813,53 +3942,36 @@ async function chat(agent2, messages, options) {
3813
3942
  "Agent is missing a context. Provide context when creating the agent."
3814
3943
  );
3815
3944
  }
3816
- if (messages.length === 0) {
3817
- throw new Error("messages must not be empty");
3818
- }
3819
- const lastItem = messages[messages.length - 1];
3820
- const lastFragment = toMessageFragment(lastItem);
3821
- const lastUIMessage = chatMessageToUIMessage(lastItem);
3822
- let assistantMsgId;
3823
- if (lastUIMessage.role === "assistant") {
3824
- context.set(lastFragment);
3825
- await context.save({ branch: false });
3826
- assistantMsgId = lastUIMessage.id;
3827
- } else {
3828
- context.set(lastFragment);
3829
- await context.save();
3830
- assistantMsgId = generateId4();
3831
- }
3832
- const uiMessages = messages.map(chatMessageToUIMessage);
3833
- let title = null;
3834
- if (!context.chat?.title) {
3835
- const firstUserMsg = uiMessages.find((m) => m.role === "user");
3836
- if (firstUserMsg) {
3837
- if (options?.generateTitle && agent2.model) {
3838
- title = await generateChatTitle({
3839
- message: firstUserMsg,
3840
- model: agent2.model,
3841
- abortSignal: options?.abortSignal
3842
- });
3843
- } else {
3844
- title = staticChatTitle(firstUserMsg);
3845
- }
3846
- await context.updateChat({ title });
3847
- }
3945
+ const head = await context.headMessage();
3946
+ if (head?.name !== "assistant") {
3947
+ throw new Error(
3948
+ "chat: expected an assistant message at head. Call context.continue(input) before chat()."
3949
+ );
3848
3950
  }
3849
- const streamContextVariables = options?.contextVariables === void 0 ? {} : options.contextVariables;
3850
- const result = await agent2.stream(streamContextVariables, {
3851
- transform: options?.transform,
3852
- abortSignal: options?.abortSignal
3853
- });
3951
+ const assistantMsgId = head.id;
3952
+ const uiMessages = await context.getMessages();
3953
+ const streamContextVariables = options.contextVariables === void 0 ? {} : options.contextVariables;
3954
+ const [title, result] = await Promise.all([
3955
+ makeTitle({
3956
+ context,
3957
+ model: agent2.model,
3958
+ generateTitle: options.generateTitle,
3959
+ abortSignal: options.abortSignal
3960
+ }),
3961
+ agent2.stream(streamContextVariables, {
3962
+ transform: options.transform,
3963
+ abortSignal: options.abortSignal
3964
+ })
3965
+ ]);
3854
3966
  const uiStream = result.toUIMessageStream({
3855
- onError: options?.onError ?? formatChatError,
3967
+ onError: options.onError ?? formatChatError,
3856
3968
  sendStart: true,
3857
3969
  sendFinish: true,
3858
3970
  sendReasoning: true,
3859
3971
  sendSources: true,
3860
3972
  originalMessages: uiMessages,
3861
3973
  generateMessageId: () => assistantMsgId,
3862
- messageMetadata: options?.messageMetadata ?? defaultChatMessageMetadata
3974
+ messageMetadata: options.messageMetadata ?? defaultChatMessageMetadata
3863
3975
  });
3864
3976
  return createUIMessageStream2({
3865
3977
  originalMessages: uiMessages,
@@ -3882,7 +3994,7 @@ async function chat(agent2, messages, options) {
3882
3994
  }
3883
3995
  const drained = sandbox.drainFileEvents?.() ?? [];
3884
3996
  const fileEvents = isAborted ? [] : drained;
3885
- const finalMetadata = await options?.finalAssistantMetadata?.(normalizedMessage);
3997
+ const finalMetadata = await options.finalAssistantMetadata?.(normalizedMessage);
3886
3998
  const mergedMetadata = {
3887
3999
  ...normalizedMessage.metadata ?? {},
3888
4000
  ...fileEvents.length > 0 ? { fileEvents } : {},
@@ -3919,9 +4031,7 @@ function sanitizeAbortedParts(parts) {
3919
4031
  sanitized.push(part);
3920
4032
  continue;
3921
4033
  }
3922
- if (part.state === "input-streaming") {
3923
- continue;
3924
- }
4034
+ if (part.state === "input-streaming") continue;
3925
4035
  sanitized.push({
3926
4036
  ...part,
3927
4037
  state: "output-error",
@@ -3946,6 +4056,19 @@ function formatChatError(error) {
3946
4056
  }
3947
4057
  return JSON.stringify(error);
3948
4058
  }
4059
+ async function makeTitle(options) {
4060
+ const titler = new TitleGenerator({ context: options.context });
4061
+ if (options.generateTitle && !options.model) {
4062
+ console.warn(
4063
+ "chat: generateTitle=true but agent.model is unset; using static title."
4064
+ );
4065
+ }
4066
+ const result = options.generateTitle && options.model ? await titler.ensure({
4067
+ model: options.model,
4068
+ abortSignal: options.abortSignal
4069
+ }) : await titler.ensureStatic();
4070
+ return result?.title ?? null;
4071
+ }
3949
4072
 
3950
4073
  // packages/context/src/lib/fragments/user.ts
3951
4074
  function identity(input) {
@@ -4432,8 +4555,7 @@ function getLocaleFromMessage(message2) {
4432
4555
  }
4433
4556
  function resolveTz(options, ctx) {
4434
4557
  if (options?.tz) return options.tz;
4435
- const locale = getLocaleFromMessage(ctx.lastMessage);
4436
- return locale?.timeZone ?? "UTC";
4558
+ return getLocaleFromMessage(ctx.currentMessage)?.timeZone ?? getLocaleFromMessage(ctx.lastMessage)?.timeZone ?? "UTC";
4437
4559
  }
4438
4560
  function toDateParts(date, tz) {
4439
4561
  const parts = new Intl.DateTimeFormat("en-CA", {
@@ -4581,7 +4703,7 @@ function dateReminder(options) {
4581
4703
  return `${diff}Date: ${currentDate}
4582
4704
  Day of Week: ${currentDay}`;
4583
4705
  },
4584
- { when: dayChanged(options), asPart: true }
4706
+ { when: dayChanged(options), asPart: false }
4585
4707
  );
4586
4708
  }
4587
4709
  function timeReminder(options) {
@@ -4599,7 +4721,7 @@ function timeReminder(options) {
4599
4721
  }
4600
4722
  return `${diff}Time: ${currentTime}`;
4601
4723
  },
4602
- { when: hourChanged(options), asPart: true }
4724
+ { when: hourChanged(options), asPart: false }
4603
4725
  );
4604
4726
  }
4605
4727
  function monthReminder(options) {
@@ -4617,7 +4739,7 @@ function monthReminder(options) {
4617
4739
  }
4618
4740
  return `${diff}Month: ${currentMonth}`;
4619
4741
  },
4620
- { when: monthChanged(options), asPart: true }
4742
+ { when: monthChanged(options), asPart: false }
4621
4743
  );
4622
4744
  }
4623
4745
  function yearReminder(options) {
@@ -4635,7 +4757,7 @@ function yearReminder(options) {
4635
4757
  }
4636
4758
  return `${diff}Year: ${currentYear}`;
4637
4759
  },
4638
- { when: yearChanged(options), asPart: true }
4760
+ { when: yearChanged(options), asPart: false }
4639
4761
  );
4640
4762
  }
4641
4763
  function seasonReminder(options) {
@@ -4653,36 +4775,30 @@ function seasonReminder(options) {
4653
4775
  }
4654
4776
  return `${diff}Season: ${currentSeason}`;
4655
4777
  },
4656
- { when: seasonChanged(options), asPart: true }
4778
+ { when: seasonChanged(options), asPart: false }
4657
4779
  );
4658
4780
  }
4659
- function localeReminder(options) {
4660
- const language = options?.language ?? "English (US)";
4661
- const timeZone = options?.timeZone ?? "UTC";
4781
+ function localeReminder() {
4662
4782
  const whenFn = (ctx) => {
4663
- const prev = getLocaleFromMessage(ctx.lastMessage);
4664
- if (!prev) return true;
4665
- return prev.language !== language || prev.timeZone !== timeZone;
4783
+ const current = getLocaleFromMessage(ctx.currentMessage);
4784
+ if (!current) return false;
4785
+ const last = getLocaleFromMessage(ctx.lastMessage);
4786
+ if (!last) return true;
4787
+ return current.language !== last.language || current.timeZone !== last.timeZone;
4666
4788
  };
4667
4789
  return reminder(
4668
4790
  (ctx) => {
4669
- const prev = getLocaleFromMessage(ctx.lastMessage);
4670
- let diff = "";
4671
- if (prev) {
4672
- diff = formatDiff([
4673
- diffLine("language", prev.language, language),
4674
- diffLine("timezone", prev.timeZone, timeZone)
4675
- ]);
4676
- }
4677
- return {
4678
- text: `${diff}Language: ${language}
4679
- Timezone: ${timeZone}`,
4680
- metadata: {
4681
- [LOCALE_METADATA_KEY]: { language, timeZone }
4682
- }
4683
- };
4791
+ const current = getLocaleFromMessage(ctx.currentMessage);
4792
+ if (!current) return "";
4793
+ const last = getLocaleFromMessage(ctx.lastMessage);
4794
+ const diff = last ? formatDiff([
4795
+ diffLine("language", last.language, current.language),
4796
+ diffLine("timezone", last.timeZone, current.timeZone)
4797
+ ]) : "";
4798
+ return `${diff}Language: ${current.language}
4799
+ Timezone: ${current.timeZone}`;
4684
4800
  },
4685
- { when: whenFn, asPart: true }
4801
+ { when: whenFn, asPart: false }
4686
4802
  );
4687
4803
  }
4688
4804
  function temporalReminder(options) {
@@ -6332,11 +6448,10 @@ function createShallowRouter(backend, ext, cwd) {
6332
6448
  executeCommand: async (raw) => {
6333
6449
  const preHook = ext.onBeforeBashCall ? (await ext.onBeforeBashCall({ command: raw })).command : raw;
6334
6450
  let transformed;
6335
- let tokens;
6451
+ let dispatch;
6336
6452
  try {
6337
6453
  transformed = ext.plugins.length > 0 ? transformer.transform(preHook).script : preHook;
6338
- const firstCmd = parse(transformed).statements[0]?.pipelines[0]?.commands[0];
6339
- tokens = firstCmd?.type === "SimpleCommand" ? tokenizeFirstCommand(transformed) : [];
6454
+ dispatch = extractDispatchTarget(parse(transformed), cwd);
6340
6455
  } catch (err) {
6341
6456
  return {
6342
6457
  stdout: "",
@@ -6345,12 +6460,12 @@ function createShallowRouter(backend, ext, cwd) {
6345
6460
  exitCode: 2
6346
6461
  };
6347
6462
  }
6348
- const [name, ...args] = tokens;
6463
+ const [name, ...args] = dispatch?.tokens ?? [];
6349
6464
  const cmd = name ? byName.get(name) : void 0;
6350
6465
  if (cmd) {
6351
6466
  return cmd.handler(args, {
6352
6467
  sandbox: backend,
6353
- cwd,
6468
+ cwd: dispatch?.cwd ?? cwd,
6354
6469
  env: ext.env,
6355
6470
  stdin: ""
6356
6471
  });
@@ -6359,20 +6474,49 @@ function createShallowRouter(backend, ext, cwd) {
6359
6474
  }
6360
6475
  };
6361
6476
  }
6362
- function tokenizeFirstCommand(commandLine) {
6363
- const ast = parse(commandLine);
6364
- const first = ast.statements[0]?.pipelines[0]?.commands[0];
6365
- if (!first || first.type !== "SimpleCommand") return [];
6366
- if (ast.statements.length > 1) return [];
6367
- if (ast.statements[0].pipelines.length > 1) return [];
6368
- if (ast.statements[0].pipelines[0].commands.length > 1) return [];
6369
- if (first.redirections.length > 0) return [];
6370
- const name = asStaticWordText(first.name);
6371
- if (!name) return [];
6477
+ function extractDispatchTarget(ast, defaultCwd) {
6478
+ if (ast.statements.length !== 1) return null;
6479
+ const statement = ast.statements[0];
6480
+ if (statement.background) return null;
6481
+ if (statement.operators.length === 0 && statement.pipelines.length === 1) {
6482
+ const tokens = tokenizeSimplePipeline(statement.pipelines[0]);
6483
+ return tokens ? { tokens, cwd: defaultCwd } : null;
6484
+ }
6485
+ if (statement.operators.length === 1 && statement.operators[0] === "&&" && statement.pipelines.length === 2) {
6486
+ const cd = simpleCommandFromPipeline(statement.pipelines[0]);
6487
+ const cdCwd = readCdTarget(cd);
6488
+ if (!cdCwd) return null;
6489
+ const tokens = tokenizeSimplePipeline(statement.pipelines[1]);
6490
+ return tokens ? { tokens, cwd: cdCwd } : null;
6491
+ }
6492
+ return null;
6493
+ }
6494
+ function simpleCommandFromPipeline(pipeline) {
6495
+ if (!pipeline) return null;
6496
+ if (pipeline.negated || pipeline.timed || pipeline.commands.length !== 1) {
6497
+ return null;
6498
+ }
6499
+ const command = pipeline.commands[0];
6500
+ return command?.type === "SimpleCommand" ? command : null;
6501
+ }
6502
+ function readCdTarget(command) {
6503
+ if (!command) return null;
6504
+ if (command.redirections.length > 0 || command.args.length !== 1) {
6505
+ return null;
6506
+ }
6507
+ const name = asStaticWordText(command.name);
6508
+ if (name !== "cd") return null;
6509
+ return asStaticWordText(command.args[0]);
6510
+ }
6511
+ function tokenizeSimplePipeline(pipeline) {
6512
+ const command = simpleCommandFromPipeline(pipeline);
6513
+ if (!command || command.redirections.length > 0) return null;
6514
+ const name = asStaticWordText(command.name);
6515
+ if (!name) return null;
6372
6516
  const args = [];
6373
- for (const arg of first.args) {
6517
+ for (const arg of command.args) {
6374
6518
  const text = asStaticWordText(arg);
6375
- if (text == null) return [];
6519
+ if (text == null) return null;
6376
6520
  args.push(text);
6377
6521
  }
6378
6522
  return [name, ...args];
@@ -9189,6 +9333,7 @@ export {
9189
9333
  SqliteStreamStore,
9190
9334
  StreamManager,
9191
9335
  StreamStore,
9336
+ TitleGenerator,
9192
9337
  TomlRenderer,
9193
9338
  ToonRenderer,
9194
9339
  XmlRenderer,
@@ -9212,7 +9357,6 @@ export {
9212
9357
  buildSubcommandRepair,
9213
9358
  chainHooks,
9214
9359
  chat,
9215
- chatMessageToUIMessage,
9216
9360
  clarification,
9217
9361
  classifies,
9218
9362
  contentIncludes,
@@ -9250,7 +9394,6 @@ export {
9250
9394
  formatResponse,
9251
9395
  fragment,
9252
9396
  fromFragment,
9253
- generateChatTitle,
9254
9397
  getFragmentData,
9255
9398
  getLocaleFromMessage,
9256
9399
  getModelsRegistry,
@@ -9313,7 +9456,6 @@ export {
9313
9456
  skillsReminder,
9314
9457
  socraticPrompting,
9315
9458
  soul,
9316
- staticChatTitle,
9317
9459
  stop,
9318
9460
  stripQuoteArtifacts,
9319
9461
  stripReminders,