@couralabs/claudin 0.1.1 → 0.1.2

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 (2) hide show
  1. package/claudin.js +308 -48
  2. package/package.json +1 -1
package/claudin.js CHANGED
@@ -5950,6 +5950,18 @@ function formatModelName(model) {
5950
5950
  const version = parts.filter((part) => NUMERIC_PART.test(part)).join(".");
5951
5951
  return ["Claude", ...words, version].filter(Boolean).join(" ");
5952
5952
  }
5953
+ // packages/events/src/models/messageText.ts
5954
+ var MAX_OUTPUT_LENGTH = 20000;
5955
+ function joinBlocks(content) {
5956
+ if (typeof content === "string")
5957
+ return content;
5958
+ if (!Array.isArray(content))
5959
+ return content === undefined ? "" : JSON.stringify(content);
5960
+ return content.map((block) => block.type === "text" ? block.text ?? "" : `[${block.type}]`).join(`
5961
+ `);
5962
+ }
5963
+ var readPromptText = (content) => joinBlocks(content);
5964
+ var clipOutput = (text) => text.slice(0, MAX_OUTPUT_LENGTH);
5953
5965
  // packages/events/src/models/modelLabel.ts
5954
5966
  function readModelLabel({ model, models }) {
5955
5967
  const row = models.find((candidate) => candidate.value === model);
@@ -6026,19 +6038,6 @@ var THEME_NAMES = [
6026
6038
  ];
6027
6039
  var themeNameSchema = _enum(THEME_NAMES);
6028
6040
  var DEFAULT_THEME_NAME = "vesper";
6029
- // packages/events/src/models/messageText.ts
6030
- var MAX_OUTPUT_LENGTH = 20000;
6031
- function joinBlocks(content) {
6032
- if (typeof content === "string")
6033
- return content;
6034
- if (!Array.isArray(content))
6035
- return content === undefined ? "" : JSON.stringify(content);
6036
- return content.map((block) => block.type === "text" ? block.text ?? "" : `[${block.type}]`).join(`
6037
- `);
6038
- }
6039
- var readPromptText = (content) => joinBlocks(content);
6040
- var clipOutput = (text) => text.slice(0, MAX_OUTPUT_LENGTH);
6041
-
6042
6041
  // packages/events/src/reducers/reduceCheckpoint.ts
6043
6042
  var isUserEntry = (entry) => entry.kind === "user";
6044
6043
  var hasToolResults = (content) => Array.isArray(content) && content.some((block) => block?.type === "tool_result");
@@ -6068,7 +6067,13 @@ var appendNotice = (state, text) => appendEntry(state, { kind: "notice", text })
6068
6067
 
6069
6068
  // packages/events/src/reducers/reduceHistory.ts
6070
6069
  var NO_MATCH_NOTICE = "No earlier prompt matches";
6071
- var listPromptTexts = (state) => state.prompts.map(readPromptText);
6070
+ var MAX_PROMPT_HISTORY = 10;
6071
+ var queuePromptHistory = (entries, text) => text === "" || entries.at(-1) === text ? entries : [...entries, text].slice(-MAX_PROMPT_HISTORY);
6072
+ var listPromptTexts = (state) => state.promptHistory;
6073
+ var loadHistory = (state, entries) => ({
6074
+ ...state,
6075
+ promptHistory: [...entries, ...state.promptHistory].slice(-MAX_PROMPT_HISTORY)
6076
+ });
6072
6077
  function readHistoryText(state) {
6073
6078
  const { history } = state.ui;
6074
6079
  if (!history)
@@ -6076,7 +6081,7 @@ function readHistoryText(state) {
6076
6081
  return listPromptTexts(state)[history.index] ?? history.draft;
6077
6082
  }
6078
6083
  function browsingPosition(state, draft) {
6079
- const size = state.prompts.length;
6084
+ const size = state.promptHistory.length;
6080
6085
  const { history } = state.ui;
6081
6086
  const browsing = history !== null && history.index < size;
6082
6087
  return { size, index: browsing ? history.index : size, draft: browsing ? history.draft : draft };
@@ -6106,20 +6111,11 @@ var agentSessionId = string2();
6106
6111
  var content = sdkValue();
6107
6112
  var agentOptions = sdkValue().optional();
6108
6113
  var effortLevelSchema = _enum(["low", "medium", "high", "xhigh", "max"]);
6109
- var permissionModeSchema = _enum([
6110
- "default",
6111
- "acceptEdits",
6112
- "bypassPermissions",
6113
- "plan",
6114
- "dontAsk",
6115
- "auto"
6116
- ]);
6117
- var DEFAULT_PERMISSION_MODE = "auto";
6114
+ var permissionModeSchema = _enum(["auto", "acceptEdits", "bypassPermissions", "dontAsk"]);
6115
+ var DEFAULT_PERMISSION_MODE = "bypassPermissions";
6118
6116
  var PERMISSION_MODE_LABELS = {
6119
- default: "Default Permission",
6120
6117
  acceptEdits: "Accept Edits Permission",
6121
6118
  bypassPermissions: "Bypass Permissions",
6122
- plan: "Plan Permission",
6123
6119
  dontAsk: "Don't Ask Permission",
6124
6120
  auto: "Automatic Permissions"
6125
6121
  };
@@ -6194,6 +6190,7 @@ var DEFAULT_CONTEXT_WINDOW = 200000;
6194
6190
  var createSessionState = (sessionId) => ({
6195
6191
  sessionId,
6196
6192
  prompts: [],
6193
+ promptHistory: [],
6197
6194
  status: "idle",
6198
6195
  model: "",
6199
6196
  permissionMode: DEFAULT_PERMISSION_MODE,
@@ -6225,7 +6222,14 @@ var createSessionState = (sessionId) => ({
6225
6222
  });
6226
6223
 
6227
6224
  // packages/events/src/reducers/carryOver.ts
6228
- var DEVICE_KEYS = ["theme", "background", "slashCommands", "models", "rateLimits"];
6225
+ var DEVICE_KEYS = [
6226
+ "theme",
6227
+ "background",
6228
+ "slashCommands",
6229
+ "models",
6230
+ "rateLimits",
6231
+ "promptHistory"
6232
+ ];
6229
6233
  var WORKSPACE_KEYS = [
6230
6234
  "model",
6231
6235
  "effort",
@@ -6502,6 +6506,14 @@ function reduceStreamEvent(state, event) {
6502
6506
  }
6503
6507
  }
6504
6508
 
6509
+ // packages/events/src/reducers/reduceTaskProgress.ts
6510
+ function reduceTaskProgress(state, message) {
6511
+ const { tool_use_id: toolUseId, summary } = message;
6512
+ if (!toolUseId || !summary)
6513
+ return state;
6514
+ return updateToolCall(state, toolUseId, (call) => ({ ...call, progress: summary }));
6515
+ }
6516
+
6505
6517
  // packages/events/src/reducers/reduceToolResults.ts
6506
6518
  function applyResult(state, block) {
6507
6519
  if (block.type !== "tool_result" || !block.tool_use_id)
@@ -6521,6 +6533,8 @@ function reduceToolResults(state, message) {
6521
6533
  function reduceAgentMessage(state, message) {
6522
6534
  switch (message.type) {
6523
6535
  case "system":
6536
+ if (message.subtype === "task_progress")
6537
+ return reduceTaskProgress(state, message);
6524
6538
  if (message.subtype !== "init")
6525
6539
  return reduceNoticeMessage(state, message);
6526
6540
  return {
@@ -6651,6 +6665,7 @@ function reducePrompt(state, content, isFollowUp) {
6651
6665
  return {
6652
6666
  ...withUser,
6653
6667
  prompts: [...state.prompts, content],
6668
+ promptHistory: queuePromptHistory(state.promptHistory, readPromptText(content)),
6654
6669
  status: "thinking",
6655
6670
  queuedTurns: isFollowUp ? state.queuedTurns + 1 : state.queuedTurns,
6656
6671
  turnStartedAt: isFollowUp ? state.turnStartedAt ?? state.now : state.now,
@@ -6784,6 +6799,8 @@ function reduceUi(state, event) {
6784
6799
  return moveHistory(state, event.delta, event.draft);
6785
6800
  case "ui.history.searched":
6786
6801
  return searchHistory(state, event.draft);
6802
+ case "ui.history.loaded":
6803
+ return loadHistory(state, event.entries);
6787
6804
  case "ui.history.reset":
6788
6805
  return resetHistory(state);
6789
6806
  case "ui.diff.viewer.moved":
@@ -7801,21 +7818,36 @@ var AGENT_TOOLS = [
7801
7818
  "TodoWrite",
7802
7819
  "AskUserQuestion",
7803
7820
  "Write",
7804
- "Edit"
7821
+ "Edit",
7822
+ "NotebookEdit"
7805
7823
  ];
7806
7824
  var AGENT_PROFILE_OPTIONS = {
7807
7825
  tools: AGENT_TOOLS,
7808
7826
  systemPrompt: ADHD_SYSTEM_PROMPT,
7809
7827
  enableFileCheckpointing: true,
7810
- extraArgs: { "replay-user-messages": null }
7828
+ extraArgs: { "replay-user-messages": null },
7829
+ permissionMode: DEFAULT_PERMISSION_MODE,
7830
+ allowDangerouslySkipPermissions: true,
7831
+ settings: {
7832
+ promptCacheTtl: "1h",
7833
+ subagentPromptCacheTtl: "1h",
7834
+ attribution: { commit: "", pr: "" }
7835
+ },
7836
+ settingSources: ["user", "project", "local"],
7837
+ skills: "all",
7838
+ agentProgressSummaries: true,
7839
+ includePartialMessages: true,
7840
+ includeHookEvents: true
7811
7841
  };
7812
7842
 
7813
7843
  // packages/agent/src/runtime/buildUserMessage.ts
7844
+ var isSlashCommand = (content) => readPromptText(content).trimStart().startsWith("/");
7814
7845
  var buildUserMessage = (content, priority) => ({
7815
7846
  type: "user",
7816
7847
  message: { role: "user", content },
7817
7848
  parent_tool_use_id: null,
7818
- priority
7849
+ priority,
7850
+ client_composed: isSlashCommand(content) ? undefined : true
7819
7851
  });
7820
7852
 
7821
7853
  // packages/agent/src/session/pickNextModel.ts
@@ -7841,7 +7873,6 @@ class AgentSession {
7841
7873
  this.query = runtime.open({
7842
7874
  prompt: this.input,
7843
7875
  options: {
7844
- includePartialMessages: true,
7845
7876
  ...baseOptions,
7846
7877
  ...AGENT_PROFILE_OPTIONS,
7847
7878
  canUseTool: this.gate.canUseTool
@@ -17616,9 +17647,7 @@ var readEffortColor = (effort) => ({
17616
17647
  max: Theme.status.danger
17617
17648
  })[effort] ?? Theme.fg.secondary;
17618
17649
  var readPermissionColor = (mode) => ({
17619
- default: Theme.fg.primary,
17620
17650
  acceptEdits: Theme.status.success,
17621
- plan: Theme.status.info,
17622
17651
  auto: Theme.accent.highlight,
17623
17652
  dontAsk: Theme.status.warning,
17624
17653
  bypassPermissions: Theme.status.danger
@@ -17758,11 +17787,125 @@ class ListPopover {
17758
17787
  // apps/tui/src/components/promptInput.ts
17759
17788
  import {
17760
17789
  BoxRenderable as BoxRenderable5,
17790
+ SyntaxStyle,
17761
17791
  TextareaRenderable,
17762
17792
  TextRenderable as TextRenderable5
17763
17793
  } from "@opentui/core";
17794
+
17795
+ // apps/tui/src/prompt/pasteAttachments.ts
17796
+ class PasteAttachments {
17797
+ replacements = new Map;
17798
+ images = new Map;
17799
+ counter = 0;
17800
+ addText(text) {
17801
+ const lines = text.split(`
17802
+ `).length;
17803
+ return this.remember(`Pasted text #${this.next()} +${lines} lines`, text);
17804
+ }
17805
+ addFile(path) {
17806
+ const name = path.split("/").at(-1) ?? path;
17807
+ return this.remember(`File #${this.next()} ${name}`, path);
17808
+ }
17809
+ addImage(image) {
17810
+ const placeholder = `[Image #${this.next()}]`;
17811
+ this.images.set(placeholder, image);
17812
+ return placeholder;
17813
+ }
17814
+ compose(text) {
17815
+ let expanded = text;
17816
+ for (const [placeholder, value] of this.replacements) {
17817
+ expanded = expanded.replaceAll(placeholder, () => value);
17818
+ }
17819
+ const images = [...this.images].map(([placeholder, image]) => ({ at: text.indexOf(placeholder), image })).filter(({ at }) => at >= 0).sort((left, right) => left.at - right.at).map(({ image }) => image);
17820
+ return { text: expanded, images };
17821
+ }
17822
+ placeholders() {
17823
+ return [...this.replacements.keys(), ...this.images.keys()];
17824
+ }
17825
+ reset() {
17826
+ this.replacements.clear();
17827
+ this.images.clear();
17828
+ this.counter = 0;
17829
+ }
17830
+ remember(label, value) {
17831
+ const placeholder = `[${label}]`;
17832
+ this.replacements.set(placeholder, value);
17833
+ return placeholder;
17834
+ }
17835
+ next() {
17836
+ this.counter += 1;
17837
+ return this.counter;
17838
+ }
17839
+ }
17840
+
17841
+ // apps/tui/src/prompt/planPaste.ts
17842
+ import { readFileSync } from "fs";
17843
+ import { extname } from "path";
17844
+ import { decodePasteBytes } from "@opentui/core";
17845
+
17846
+ // apps/tui/src/prompt/imageBlock.ts
17847
+ var IMAGE_MEDIA_TYPES = ["image/gif", "image/jpeg", "image/png", "image/webp"];
17848
+ var readImageMediaType = (mediaType) => IMAGE_MEDIA_TYPES.find((known) => known === mediaType);
17849
+ var buildImageBlock = (mediaType, bytes) => ({
17850
+ type: "image",
17851
+ source: { type: "base64", media_type: mediaType, data: Buffer.from(bytes).toString("base64") }
17852
+ });
17853
+
17854
+ // apps/tui/src/prompt/pastedPaths.ts
17855
+ import { statSync } from "fs";
17856
+ import { homedir as homedir6 } from "os";
17857
+ import { isAbsolute } from "path";
17858
+ var unwrapPath = (raw) => raw.trim().replace(/^(['"])(.*)\1$/, "$2").replace(/^file:\/\//, "").replace(/^~(?=\/)/, homedir6()).replace(/\\(.)/g, "$1");
17859
+ var isExistingFile = (path) => {
17860
+ try {
17861
+ return isAbsolute(path) && statSync(path).isFile();
17862
+ } catch {
17863
+ return false;
17864
+ }
17865
+ };
17866
+ function parsePastedPaths(text) {
17867
+ const lines = text.split(`
17868
+ `).map((line) => line.trim()).filter(Boolean);
17869
+ const paths = lines.flatMap((line) => isExistingFile(unwrapPath(line)) ? [line] : line.split(/(?<!\\)\s+/)).map(unwrapPath);
17870
+ return paths.length > 0 && paths.every(isExistingFile) ? paths : [];
17871
+ }
17872
+
17873
+ // apps/tui/src/prompt/planPaste.ts
17874
+ var LONG_PASTE_LINES = 5;
17875
+ var LONG_PASTE_CHARS = 500;
17876
+ var IMAGE_TYPES = {
17877
+ ".png": "image/png",
17878
+ ".jpg": "image/jpeg",
17879
+ ".jpeg": "image/jpeg",
17880
+ ".gif": "image/gif",
17881
+ ".webp": "image/webp"
17882
+ };
17883
+ var isLongPaste = (text) => text.length > LONG_PASTE_CHARS || text.split(`
17884
+ `).length > LONG_PASTE_LINES;
17885
+ function attachPath(path, attachments) {
17886
+ const mediaType = IMAGE_TYPES[extname(path).toLowerCase()];
17887
+ if (!mediaType)
17888
+ return attachments.addFile(path);
17889
+ return attachments.addImage(buildImageBlock(mediaType, readFileSync(path)));
17890
+ }
17891
+ function planPaste(event, attachments) {
17892
+ const mediaType = readImageMediaType(event.metadata?.mimeType);
17893
+ if (mediaType) {
17894
+ return attachments.addImage(buildImageBlock(mediaType, event.bytes));
17895
+ }
17896
+ if (event.metadata?.kind === "binary")
17897
+ return;
17898
+ const text = decodePasteBytes(event.bytes);
17899
+ const paths = parsePastedPaths(text);
17900
+ if (paths.length > 0)
17901
+ return paths.map((path) => attachPath(path, attachments)).join(" ");
17902
+ return isLongPaste(text) ? attachments.addText(text) : undefined;
17903
+ }
17904
+
17905
+ // apps/tui/src/components/promptInput.ts
17764
17906
  var MAX_INPUT_LINES = 8;
17765
17907
  var BUSY_TITLE = " Ctrl+Q (queue) Ctrl+W (steer) ";
17908
+ var ATTACHMENT_STYLE = "attachment";
17766
17909
  var PLACEHOLDER = "Type your prompt, /slash command, or @file reference...";
17767
17910
  var KEY_BINDINGS = [
17768
17911
  { name: "return", action: "submit" },
@@ -17776,6 +17919,9 @@ class PromptInput {
17776
17919
  renderable;
17777
17920
  textarea;
17778
17921
  chevron;
17922
+ attachments = new PasteAttachments;
17923
+ attachmentStyle;
17924
+ attachmentStyleId = 0;
17779
17925
  focused = false;
17780
17926
  constructor(renderer, handlers) {
17781
17927
  this.renderable = new BoxRenderable5(renderer, {
@@ -17787,14 +17933,18 @@ class PromptInput {
17787
17933
  flexShrink: 0,
17788
17934
  paddingLeft: 1
17789
17935
  });
17936
+ this.attachmentStyle = SyntaxStyle.create(renderer.nativeScene);
17790
17937
  this.textarea = new TextareaRenderable(renderer, {
17791
17938
  flexGrow: 1,
17792
17939
  height: 1,
17793
17940
  placeholder: PLACEHOLDER,
17794
17941
  wrapMode: "word",
17795
17942
  keyBindings: KEY_BINDINGS,
17943
+ syntaxStyle: this.attachmentStyle,
17944
+ onPaste: (event) => this.insertPlaceholder(event),
17796
17945
  onSubmit: () => handlers.onSubmit(this.text),
17797
17946
  onContentChange: () => {
17947
+ this.markAttachments();
17798
17948
  this.fitHeight();
17799
17949
  handlers.onChange(this.text, this.textarea.cursorOffset);
17800
17950
  }
@@ -17814,8 +17964,19 @@ class PromptInput {
17814
17964
  this.textarea.setText(text);
17815
17965
  this.textarea.cursorOffset = cursor;
17816
17966
  }
17967
+ compose() {
17968
+ return this.attachments.compose(this.text);
17969
+ }
17970
+ insertPlaceholder(event) {
17971
+ const placeholder = planPaste(event, this.attachments);
17972
+ if (placeholder === undefined)
17973
+ return;
17974
+ event.preventDefault();
17975
+ this.textarea.insertText(placeholder);
17976
+ }
17817
17977
  clear() {
17818
17978
  this.textarea.setText("");
17979
+ this.attachments.reset();
17819
17980
  }
17820
17981
  setFocused(focused) {
17821
17982
  if (focused === this.focused)
@@ -17842,6 +18003,26 @@ class PromptInput {
17842
18003
  this.textarea.focusedTextColor = Theme.fg.primary;
17843
18004
  this.textarea.backgroundColor = Theme.bg.default;
17844
18005
  this.textarea.focusedBackgroundColor = Theme.bg.default;
18006
+ this.attachmentStyleId = this.attachmentStyle.registerStyle(ATTACHMENT_STYLE, {
18007
+ fg: Theme.accent.highlight,
18008
+ bold: true
18009
+ });
18010
+ this.markAttachments();
18011
+ }
18012
+ markAttachments() {
18013
+ const { extmarks } = this.textarea;
18014
+ extmarks.clear();
18015
+ const text = this.text;
18016
+ for (const placeholder of this.attachments.placeholders()) {
18017
+ for (let at = text.indexOf(placeholder);at >= 0; at = text.indexOf(placeholder, at + 1)) {
18018
+ extmarks.create({
18019
+ start: at,
18020
+ end: at + placeholder.length,
18021
+ virtual: true,
18022
+ styleId: this.attachmentStyleId
18023
+ });
18024
+ }
18025
+ }
17845
18026
  }
17846
18027
  fitHeight() {
17847
18028
  const wanted = Math.max(this.textarea.lineCount, this.textarea.virtualLineCount, 1);
@@ -18580,7 +18761,9 @@ var SUMMARY_LENGTH = 80;
18580
18761
  var SUMMARY_KEYS = ["file_path", "path", "command", "pattern", "query", "url", "description"];
18581
18762
  var SEARCH_TOOLS = new Set(["Glob", "Grep"]);
18582
18763
  var summarizeSearch = ({ pattern, path }) => typeof path === "string" ? `${pattern} in ${path}` : String(pattern);
18583
- function summarizeToolCall({ name, input }) {
18764
+ function summarizeToolCall({ name, input, status, progress }) {
18765
+ if (status === "running" && progress)
18766
+ return truncateText(progress, SUMMARY_LENGTH);
18584
18767
  if (SEARCH_TOOLS.has(name) && typeof input.pattern === "string") {
18585
18768
  return truncateText(summarizeSearch(input), SUMMARY_LENGTH);
18586
18769
  }
@@ -18875,6 +19058,8 @@ import {
18875
19058
  StyledText as StyledText11,
18876
19059
  TextRenderable as TextRenderable12
18877
19060
  } from "@opentui/core";
19061
+ var USER_LINES = 10;
19062
+
18878
19063
  class UserMessageView {
18879
19064
  renderable;
18880
19065
  text;
@@ -18903,7 +19088,7 @@ class UserMessageView {
18903
19088
  this.entry = entry;
18904
19089
  this.text.content = new StyledText11([
18905
19090
  bold7(fg12(Theme.accent.brand)("\u276F ")),
18906
- bold7(fg12(Theme.fg.primary)(entry.text))
19091
+ bold7(fg12(Theme.fg.primary)(clipLines(entry.text, USER_LINES)))
18907
19092
  ]);
18908
19093
  this.clock.content = new StyledText11([fg12(Theme.fg.muted)(formatClock(entry.at))]);
18909
19094
  }
@@ -19544,23 +19729,27 @@ var DELIVERY_REQUESTS = {
19544
19729
  steer: "agent.prompt.steer.requested"
19545
19730
  };
19546
19731
  var BUSY_NOTICE = "Agent is busy: Ctrl+Q queues, Ctrl+W steers, Esc Esc interrupts the turn";
19547
- function deliverPrompt(store, content, delivery) {
19732
+ var buildContent = (text, images) => images.length > 0 ? [{ type: "text", text }, ...images] : text;
19733
+ function deliverPrompt(store, content, delivery, images) {
19548
19734
  const busy = isAgentBusy(store);
19549
19735
  if (busy && delivery === "send") {
19550
19736
  store.emit({ type: "ui.notice.posted", text: BUSY_NOTICE });
19551
19737
  return false;
19552
19738
  }
19553
- return requestAgent(store, { type: DELIVERY_REQUESTS[busy ? delivery : "send"], content });
19739
+ return requestAgent(store, {
19740
+ type: DELIVERY_REQUESTS[busy ? delivery : "send"],
19741
+ content: buildContent(content, images)
19742
+ });
19554
19743
  }
19555
- function submitPrompt(store, text, delivery = "send") {
19744
+ function submitPrompt(store, text, delivery = "send", images = []) {
19556
19745
  const content = text.trim();
19557
- if (!content)
19746
+ if (!content && images.length === 0)
19558
19747
  return false;
19559
19748
  if (content.startsWith("/") && !isAgentCommand(store.state, content)) {
19560
19749
  runSlashCommand(store, content);
19561
19750
  return true;
19562
19751
  }
19563
- return deliverPrompt(store, content, delivery);
19752
+ return deliverPrompt(store, content, delivery, images);
19564
19753
  }
19565
19754
  function answerApproval(store, decision, toolUseId) {
19566
19755
  const target = toolUseId ?? findAwaitingCalls(store.state)[0]?.id;
@@ -19593,11 +19782,9 @@ var showHistoryText = (store, editor) => {
19593
19782
  editor.replace(text);
19594
19783
  };
19595
19784
  function routeHistoryKey(key, store, editor) {
19596
- const { ui, prompts } = store.state;
19597
- if (ui.focus !== "input" || key.name !== "up" && key.name !== "down")
19785
+ if (store.state.ui.focus !== "input" || !key.shift)
19598
19786
  return false;
19599
- const starting = key.name === "up" && editor.read() === "" && prompts.length > 0;
19600
- if (!ui.history && !starting)
19787
+ if (key.name !== "up" && key.name !== "down")
19601
19788
  return false;
19602
19789
  const delta = key.name === "up" ? -1 : 1;
19603
19790
  store.emit({ type: "ui.history.moved", delta, draft: editor.read() });
@@ -19917,6 +20104,60 @@ class TurnNotifier {
19917
20104
  }
19918
20105
  }
19919
20106
 
20107
+ // apps/tui/src/prompt/historyFile.ts
20108
+ import { homedir as homedir7 } from "os";
20109
+ import { join as join6 } from "path";
20110
+ var HISTORY_ROOT = join6(homedir7(), ".claudin", "prompts");
20111
+ var buildHistoryPath = (cwd, root = HISTORY_ROOT) => join6(root, cwd.replace(/[^a-zA-Z0-9]/g, "-"), "history.jsonl");
20112
+ var parseLine = (line) => {
20113
+ try {
20114
+ const { text } = JSON.parse(line);
20115
+ return typeof text === "string" && text !== "" ? text : undefined;
20116
+ } catch {
20117
+ return;
20118
+ }
20119
+ };
20120
+ async function readHistoryFile(path) {
20121
+ const content = await Bun.file(path).text().catch(() => "");
20122
+ const entries = content.split(`
20123
+ `).flatMap((line) => parseLine(line) ?? []);
20124
+ return entries.slice(-MAX_PROMPT_HISTORY);
20125
+ }
20126
+ async function writeHistoryFile(path, entries) {
20127
+ const lines = entries.slice(-MAX_PROMPT_HISTORY).map((text) => JSON.stringify({ text }));
20128
+ await Bun.write(path, lines.length > 0 ? `${lines.join(`
20129
+ `)}
20130
+ ` : "");
20131
+ }
20132
+
20133
+ // apps/tui/src/prompt/historySaver.ts
20134
+ var SAVE_DELAY_MS = 400;
20135
+
20136
+ class HistorySaver {
20137
+ path;
20138
+ delayMs;
20139
+ timer;
20140
+ pending;
20141
+ constructor(path, delayMs = SAVE_DELAY_MS) {
20142
+ this.path = path;
20143
+ this.delayMs = delayMs;
20144
+ }
20145
+ schedule(entries, draft) {
20146
+ this.pending = queuePromptHistory(entries, draft.trim());
20147
+ clearTimeout(this.timer);
20148
+ this.timer = setTimeout(() => void this.flush(), this.delayMs);
20149
+ }
20150
+ async flush() {
20151
+ clearTimeout(this.timer);
20152
+ const entries = this.pending;
20153
+ this.pending = undefined;
20154
+ if (entries)
20155
+ await writeHistoryFile(this.path, entries).catch(() => {
20156
+ return;
20157
+ });
20158
+ }
20159
+ }
20160
+
19920
20161
  // apps/tui/src/store/appStore.ts
19921
20162
  class AppStore {
19922
20163
  client;
@@ -19958,8 +20199,8 @@ class AppStore {
19958
20199
  }
19959
20200
 
19960
20201
  // apps/tui/src/theme/syntaxStyle.ts
19961
- import { SyntaxStyle } from "@opentui/core";
19962
- var createSyntaxStyle = (renderer) => SyntaxStyle.fromStyles({
20202
+ import { SyntaxStyle as SyntaxStyle2 } from "@opentui/core";
20203
+ var createSyntaxStyle = (renderer) => SyntaxStyle2.fromStyles({
19963
20204
  default: { fg: Theme.fg.primary },
19964
20205
  "markup.heading": { fg: Theme.accent.brand, bold: true },
19965
20206
  "markup.strong": { fg: Theme.fg.primary, bold: true },
@@ -20002,15 +20243,28 @@ async function startTui(client, options = {}) {
20002
20243
  });
20003
20244
  const diffViewer = new DiffViewerPanel(activeRenderer, syntaxStyle);
20004
20245
  const submitInput = (delivery) => {
20005
- if (!submitPrompt(store, input.text, delivery))
20246
+ const draft = input.compose();
20247
+ if (!submitPrompt(store, draft.text, delivery, draft.images))
20006
20248
  return;
20007
20249
  input.clear();
20008
20250
  if (store.state.ui.history)
20009
20251
  store.emit({ type: "ui.history.reset" });
20010
20252
  };
20253
+ const historyPath = options.historyPath ?? buildHistoryPath(process.cwd());
20254
+ const historySaver = new HistorySaver(historyPath);
20255
+ let destroyed = false;
20256
+ const saveDraft = (text) => {
20257
+ const { ui, promptHistory } = store.state;
20258
+ if (ui.history && text === readHistoryText(store.state))
20259
+ return;
20260
+ historySaver.schedule(promptHistory, text);
20261
+ };
20011
20262
  const input = new PromptInput(activeRenderer, {
20012
20263
  onSubmit: () => submitInput("send"),
20013
- onChange: (text, cursor) => syncAutocomplete(store, text, cursor)
20264
+ onChange: (text, cursor) => {
20265
+ syncAutocomplete(store, text, cursor);
20266
+ saveDraft(text);
20267
+ }
20014
20268
  });
20015
20269
  const root = new BoxRenderable15(activeRenderer, {
20016
20270
  width: "100%",
@@ -20124,9 +20378,15 @@ async function startTui(client, options = {}) {
20124
20378
  activeRenderer.on("destroy", () => {
20125
20379
  clearInterval(clock);
20126
20380
  clearInterval(rateLimitPoll);
20381
+ destroyed = true;
20382
+ historySaver.flush();
20127
20383
  options.onExit?.(store.state.sessionId);
20128
20384
  store.close();
20129
20385
  });
20386
+ readHistoryFile(historyPath).then((entries) => {
20387
+ if (!destroyed)
20388
+ store.emit({ type: "ui.history.loaded", entries });
20389
+ });
20130
20390
  render(store.state);
20131
20391
  input.setFocused(true);
20132
20392
  if (options.attach)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@couralabs/claudin",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "claudin": "claudin.js"