scream-code 0.7.8 → 0.7.10
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/{app-AVBOJrWs.mjs → app-BJoewl4C.mjs} +289 -16
- package/dist/main.mjs +1 -1
- package/package.json +4 -4
|
@@ -121994,7 +121994,7 @@ function optionalBuildString(value) {
|
|
|
121994
121994
|
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
121995
121995
|
}
|
|
121996
121996
|
const SCREAM_BUILD_INFO = {
|
|
121997
|
-
version: optionalBuildString("0.7.
|
|
121997
|
+
version: optionalBuildString("0.7.10"),
|
|
121998
121998
|
channel: optionalBuildString(""),
|
|
121999
121999
|
commit: optionalBuildString(""),
|
|
122000
122000
|
buildTarget: optionalBuildString("darwin-arm64")
|
|
@@ -123815,6 +123815,15 @@ function effectiveThinkingLevel(model, thinkingDraft) {
|
|
|
123815
123815
|
if (levels.includes(thinkingDraft)) return thinkingDraft;
|
|
123816
123816
|
return levels[0] ?? "off";
|
|
123817
123817
|
}
|
|
123818
|
+
function modelHasImageIn(model) {
|
|
123819
|
+
return model.capabilities?.includes("image_in") === true;
|
|
123820
|
+
}
|
|
123821
|
+
function modelHasVideoIn(model) {
|
|
123822
|
+
return model.capabilities?.includes("video_in") === true;
|
|
123823
|
+
}
|
|
123824
|
+
function modelHasAudioIn(model) {
|
|
123825
|
+
return model.capabilities?.includes("audio_in") === true;
|
|
123826
|
+
}
|
|
123818
123827
|
var ModelSelectorComponent = class extends Container {
|
|
123819
123828
|
focused = false;
|
|
123820
123829
|
opts;
|
|
@@ -123849,11 +123858,13 @@ var ModelSelectorComponent = class extends Container {
|
|
|
123849
123858
|
if (matchesKey(data, Key.left)) {
|
|
123850
123859
|
const nextIdx = idx <= 0 ? levels.length - 1 : idx - 1;
|
|
123851
123860
|
this.thinkingDraft = levels[nextIdx];
|
|
123861
|
+
this.opts.onChangeThinking?.(selected.alias, this.thinkingDraft);
|
|
123852
123862
|
return;
|
|
123853
123863
|
}
|
|
123854
123864
|
if (matchesKey(data, Key.right)) {
|
|
123855
123865
|
const nextIdx = idx === -1 || idx >= levels.length - 1 ? 0 : idx + 1;
|
|
123856
123866
|
this.thinkingDraft = levels[nextIdx];
|
|
123867
|
+
this.opts.onChangeThinking?.(selected.alias, this.thinkingDraft);
|
|
123857
123868
|
return;
|
|
123858
123869
|
}
|
|
123859
123870
|
}
|
|
@@ -123874,7 +123885,7 @@ var ModelSelectorComponent = class extends Container {
|
|
|
123874
123885
|
const choices = view.items;
|
|
123875
123886
|
const navParts = ["↑↓ 模型", "←→ 思考等级"];
|
|
123876
123887
|
if (view.page.pageCount > 1) navParts.push("PgUp/PgDn 翻页");
|
|
123877
|
-
navParts.push("Enter
|
|
123888
|
+
navParts.push("Enter 切换模型", "Esc 取消");
|
|
123878
123889
|
const titleSuffix = searchable && view.query.length === 0 ? chalk.hex(colors.textMuted)(" (输入搜索)") : "";
|
|
123879
123890
|
const lines = [chalk.hex(colors.primary)("─".repeat(width)), chalk.hex(colors.primary).bold(" 选择模型") + titleSuffix];
|
|
123880
123891
|
if (searchable && view.query.length > 0) lines.push(chalk.hex(colors.primary)(" 搜索:") + chalk.hex(colors.text)(view.query));
|
|
@@ -123893,10 +123904,17 @@ var ModelSelectorComponent = class extends Container {
|
|
|
123893
123904
|
lines.push(line);
|
|
123894
123905
|
}
|
|
123895
123906
|
lines.push("");
|
|
123896
|
-
lines.push(chalk.hex(colors.textMuted)(" Thinking"));
|
|
123907
|
+
lines.push(chalk.hex(colors.textMuted)(" Thinking(全局设置)"));
|
|
123897
123908
|
const selected = choices[view.selectedIndex];
|
|
123898
123909
|
if (selected !== void 0) lines.push(this.renderThinkingControl(selected.model));
|
|
123899
123910
|
lines.push("");
|
|
123911
|
+
lines.push(chalk.hex(colors.textMuted)(" 多模态能力"));
|
|
123912
|
+
if (selected !== void 0) {
|
|
123913
|
+
lines.push(this.renderImageControl(selected.model));
|
|
123914
|
+
lines.push(this.renderVideoControl(selected.model));
|
|
123915
|
+
lines.push(this.renderAudioControl(selected.model));
|
|
123916
|
+
}
|
|
123917
|
+
lines.push("");
|
|
123900
123918
|
if (view.page.pageCount > 1) lines.push(chalk.hex(colors.textMuted)(` Page ${String(view.page.page + 1)}/${String(view.page.pageCount)}`));
|
|
123901
123919
|
lines.push(chalk.hex(colors.primary)("─".repeat(width)));
|
|
123902
123920
|
return lines.map((line) => truncateToWidth(line, width));
|
|
@@ -123912,6 +123930,21 @@ var ModelSelectorComponent = class extends Container {
|
|
|
123912
123930
|
if (availability === "unsupported") return `${line} ${chalk.hex(colors.textMuted)("unsupported")}`;
|
|
123913
123931
|
return line;
|
|
123914
123932
|
}
|
|
123933
|
+
renderImageControl(model) {
|
|
123934
|
+
const { colors } = this.opts;
|
|
123935
|
+
if (modelHasImageIn(model)) return ` ${chalk.hex(colors.textMuted)("识图")}: ${chalk.hex(colors.success).bold("✓ 支持")}`;
|
|
123936
|
+
return ` ${chalk.hex(colors.textMuted)("识图")}: ${chalk.hex(colors.textDim)("✗ 不支持")}`;
|
|
123937
|
+
}
|
|
123938
|
+
renderVideoControl(model) {
|
|
123939
|
+
const { colors } = this.opts;
|
|
123940
|
+
if (modelHasVideoIn(model)) return ` ${chalk.hex(colors.textMuted)("视频")}: ${chalk.hex(colors.success).bold("✓ 支持")}`;
|
|
123941
|
+
return ` ${chalk.hex(colors.textMuted)("视频")}: ${chalk.hex(colors.textDim)("✗ 不支持")}`;
|
|
123942
|
+
}
|
|
123943
|
+
renderAudioControl(model) {
|
|
123944
|
+
const { colors } = this.opts;
|
|
123945
|
+
if (modelHasAudioIn(model)) return ` ${chalk.hex(colors.textMuted)("音频")}: ${chalk.hex(colors.success).bold("✓ 支持")}`;
|
|
123946
|
+
return ` ${chalk.hex(colors.textMuted)("音频")}: ${chalk.hex(colors.textDim)("✗ 不支持")}`;
|
|
123947
|
+
}
|
|
123915
123948
|
};
|
|
123916
123949
|
//#endregion
|
|
123917
123950
|
//#region src/tui/components/dialogs/text-input-dialog.ts
|
|
@@ -124124,6 +124157,33 @@ const THINKING_OPTIONS = [
|
|
|
124124
124157
|
label: "高强度思考"
|
|
124125
124158
|
}
|
|
124126
124159
|
];
|
|
124160
|
+
const IMAGE_OPTIONS = [{
|
|
124161
|
+
value: "off",
|
|
124162
|
+
label: "关闭识图",
|
|
124163
|
+
description: "模型不支持图片输入时请选择此项"
|
|
124164
|
+
}, {
|
|
124165
|
+
value: "on",
|
|
124166
|
+
label: "开启识图",
|
|
124167
|
+
description: "模型支持图片输入时开启,允许发送图片"
|
|
124168
|
+
}];
|
|
124169
|
+
const VIDEO_OPTIONS = [{
|
|
124170
|
+
value: "off",
|
|
124171
|
+
label: "关闭视频",
|
|
124172
|
+
description: "模型不支持视频输入时请选择此项"
|
|
124173
|
+
}, {
|
|
124174
|
+
value: "on",
|
|
124175
|
+
label: "开启视频",
|
|
124176
|
+
description: "模型支持视频输入时开启,允许发送视频"
|
|
124177
|
+
}];
|
|
124178
|
+
const AUDIO_OPTIONS = [{
|
|
124179
|
+
value: "off",
|
|
124180
|
+
label: "关闭音频",
|
|
124181
|
+
description: "模型不支持音频输入时请选择此项"
|
|
124182
|
+
}, {
|
|
124183
|
+
value: "on",
|
|
124184
|
+
label: "开启音频",
|
|
124185
|
+
description: "模型支持音频输入时开启,允许发送音频"
|
|
124186
|
+
}];
|
|
124127
124187
|
function promptWireType(host) {
|
|
124128
124188
|
return new Promise((resolve) => {
|
|
124129
124189
|
const picker = new ChoicePickerComponent({
|
|
@@ -124177,6 +124237,63 @@ function promptThinkingMode(host) {
|
|
|
124177
124237
|
host.mountEditorReplacement(picker);
|
|
124178
124238
|
});
|
|
124179
124239
|
}
|
|
124240
|
+
function promptImageMode(host) {
|
|
124241
|
+
return new Promise((resolve) => {
|
|
124242
|
+
const picker = new ChoicePickerComponent({
|
|
124243
|
+
title: "多模态配置 · 图片输入",
|
|
124244
|
+
hint: "模型不支持请选择关闭",
|
|
124245
|
+
options: IMAGE_OPTIONS,
|
|
124246
|
+
colors: host.state.theme.colors,
|
|
124247
|
+
onSelect: (value) => {
|
|
124248
|
+
host.restoreEditor();
|
|
124249
|
+
resolve(value === "on");
|
|
124250
|
+
},
|
|
124251
|
+
onCancel: () => {
|
|
124252
|
+
host.restoreEditor();
|
|
124253
|
+
resolve(void 0);
|
|
124254
|
+
}
|
|
124255
|
+
});
|
|
124256
|
+
host.mountEditorReplacement(picker);
|
|
124257
|
+
});
|
|
124258
|
+
}
|
|
124259
|
+
function promptVideoMode(host) {
|
|
124260
|
+
return new Promise((resolve) => {
|
|
124261
|
+
const picker = new ChoicePickerComponent({
|
|
124262
|
+
title: "多模态配置 · 视频输入",
|
|
124263
|
+
hint: "模型不支持请选择关闭",
|
|
124264
|
+
options: VIDEO_OPTIONS,
|
|
124265
|
+
colors: host.state.theme.colors,
|
|
124266
|
+
onSelect: (value) => {
|
|
124267
|
+
host.restoreEditor();
|
|
124268
|
+
resolve(value === "on");
|
|
124269
|
+
},
|
|
124270
|
+
onCancel: () => {
|
|
124271
|
+
host.restoreEditor();
|
|
124272
|
+
resolve(void 0);
|
|
124273
|
+
}
|
|
124274
|
+
});
|
|
124275
|
+
host.mountEditorReplacement(picker);
|
|
124276
|
+
});
|
|
124277
|
+
}
|
|
124278
|
+
function promptAudioMode(host) {
|
|
124279
|
+
return new Promise((resolve) => {
|
|
124280
|
+
const picker = new ChoicePickerComponent({
|
|
124281
|
+
title: "多模态配置 · 音频输入",
|
|
124282
|
+
hint: "模型不支持请选择关闭",
|
|
124283
|
+
options: AUDIO_OPTIONS,
|
|
124284
|
+
colors: host.state.theme.colors,
|
|
124285
|
+
onSelect: (value) => {
|
|
124286
|
+
host.restoreEditor();
|
|
124287
|
+
resolve(value === "on");
|
|
124288
|
+
},
|
|
124289
|
+
onCancel: () => {
|
|
124290
|
+
host.restoreEditor();
|
|
124291
|
+
resolve(void 0);
|
|
124292
|
+
}
|
|
124293
|
+
});
|
|
124294
|
+
host.mountEditorReplacement(picker);
|
|
124295
|
+
});
|
|
124296
|
+
}
|
|
124180
124297
|
//#endregion
|
|
124181
124298
|
//#region src/tui/commands/auth.ts
|
|
124182
124299
|
async function handleConnectCommand(host, args) {
|
|
@@ -124319,15 +124436,21 @@ async function handleDiyConfig(host) {
|
|
|
124319
124436
|
const maxContextTokens = parseInt(maxContextStr, 10) || 131072;
|
|
124320
124437
|
const thinkingLevel = await promptThinkingMode(host);
|
|
124321
124438
|
if (thinkingLevel === void 0) return;
|
|
124439
|
+
const imageEnabled = await promptImageMode(host);
|
|
124440
|
+
if (imageEnabled === void 0) return;
|
|
124441
|
+
const videoEnabled = await promptVideoMode(host);
|
|
124442
|
+
if (videoEnabled === void 0) return;
|
|
124443
|
+
const audioEnabled = await promptAudioMode(host);
|
|
124444
|
+
if (audioEnabled === void 0) return;
|
|
124322
124445
|
const providerId = `custom-${modelId.replaceAll(/[^A-Za-z0-9._-]/g, "-")}`;
|
|
124323
124446
|
const catalogModel = {
|
|
124324
124447
|
id: modelId,
|
|
124325
124448
|
name: modelId,
|
|
124326
124449
|
capability: {
|
|
124327
124450
|
max_context_tokens: maxContextTokens,
|
|
124328
|
-
image_in:
|
|
124329
|
-
video_in:
|
|
124330
|
-
audio_in:
|
|
124451
|
+
image_in: imageEnabled,
|
|
124452
|
+
video_in: videoEnabled,
|
|
124453
|
+
audio_in: audioEnabled,
|
|
124331
124454
|
thinking: thinkingLevel !== "off",
|
|
124332
124455
|
tool_use: true
|
|
124333
124456
|
},
|
|
@@ -125629,11 +125752,68 @@ function showModelPicker(host, selectedValue = host.state.appState.model) {
|
|
|
125629
125752
|
host.restoreEditor();
|
|
125630
125753
|
performModelSwitch(host, alias, thinkingLevel);
|
|
125631
125754
|
},
|
|
125755
|
+
onChangeThinking: (alias, level) => {
|
|
125756
|
+
changeThinkingLevel(host, alias, level);
|
|
125757
|
+
},
|
|
125632
125758
|
onCancel: () => {
|
|
125633
125759
|
host.restoreEditor();
|
|
125634
125760
|
}
|
|
125635
125761
|
}));
|
|
125636
125762
|
}
|
|
125763
|
+
/**
|
|
125764
|
+
* Apply a thinking-level change immediately (←/→ keys in the model selector).
|
|
125765
|
+
* Decoupled from performModelSwitch so the user can cycle thinking on the
|
|
125766
|
+
* currently-selected model without pressing Enter. If the alias is the active
|
|
125767
|
+
* model, the running session is updated too; otherwise only the default is
|
|
125768
|
+
* persisted.
|
|
125769
|
+
*/
|
|
125770
|
+
async function changeThinkingLevel(host, alias, level) {
|
|
125771
|
+
if (isBusy(host.state.appState)) {
|
|
125772
|
+
host.showError("Cannot change thinking while streaming — press Esc or Ctrl-C first.");
|
|
125773
|
+
return;
|
|
125774
|
+
}
|
|
125775
|
+
const prevLevel = host.state.appState.thinkingLevel;
|
|
125776
|
+
const isActiveModel = alias === host.state.appState.model;
|
|
125777
|
+
if (isActiveModel && level !== prevLevel) {
|
|
125778
|
+
const session = host.session;
|
|
125779
|
+
try {
|
|
125780
|
+
if (session === void 0) await host.authFlow.activateModelAfterLogin(alias, level);
|
|
125781
|
+
else await session.setThinking(level);
|
|
125782
|
+
} catch (error) {
|
|
125783
|
+
const msg = formatErrorMessage(error);
|
|
125784
|
+
host.showError(`Failed to set thinking: ${msg}`);
|
|
125785
|
+
return;
|
|
125786
|
+
}
|
|
125787
|
+
host.setAppState({ thinkingLevel: level });
|
|
125788
|
+
}
|
|
125789
|
+
let persisted = false;
|
|
125790
|
+
try {
|
|
125791
|
+
persisted = await persistThinkingDefault(host, alias, level);
|
|
125792
|
+
} catch (error) {
|
|
125793
|
+
const msg = formatErrorMessage(error);
|
|
125794
|
+
host.showError(`Failed to save thinking default: ${msg}`);
|
|
125795
|
+
return;
|
|
125796
|
+
}
|
|
125797
|
+
const status = isActiveModel && level !== prevLevel ? `Thinking set to ${level} for ${alias}.` : persisted ? `Saved thinking ${level} as default for ${alias}.` : `Thinking already ${level} for ${alias}.`;
|
|
125798
|
+
host.showStatus(status, host.state.theme.colors.success);
|
|
125799
|
+
}
|
|
125800
|
+
async function persistThinkingDefault(host, alias, level) {
|
|
125801
|
+
const config = await host.harness.getConfig({ reload: true });
|
|
125802
|
+
const effectiveThinking = level !== "off";
|
|
125803
|
+
const existingEffort = config.thinking?.effort;
|
|
125804
|
+
const newEffort = effectiveThinking ? level : existingEffort;
|
|
125805
|
+
if (!(config.defaultModel === alias)) return false;
|
|
125806
|
+
if (config.defaultThinking === effectiveThinking && existingEffort === newEffort) return false;
|
|
125807
|
+
await host.harness.setConfig({
|
|
125808
|
+
defaultThinking: effectiveThinking,
|
|
125809
|
+
thinking: {
|
|
125810
|
+
...config.thinking,
|
|
125811
|
+
mode: effectiveThinking ? "on" : "off",
|
|
125812
|
+
effort: newEffort
|
|
125813
|
+
}
|
|
125814
|
+
});
|
|
125815
|
+
return true;
|
|
125816
|
+
}
|
|
125637
125817
|
async function performModelSwitch(host, alias, thinkingLevel) {
|
|
125638
125818
|
if (isBusy(host.state.appState)) {
|
|
125639
125819
|
host.showError("Cannot switch models while streaming — press Esc or Ctrl-C first.");
|
|
@@ -125641,7 +125821,9 @@ async function performModelSwitch(host, alias, thinkingLevel) {
|
|
|
125641
125821
|
}
|
|
125642
125822
|
const prevModel = host.state.appState.model;
|
|
125643
125823
|
const prevThinkingLevel = host.state.appState.thinkingLevel;
|
|
125644
|
-
const
|
|
125824
|
+
const modelChanged = alias !== prevModel;
|
|
125825
|
+
const thinkingChanged = thinkingLevel !== prevThinkingLevel;
|
|
125826
|
+
const needsSessionActivation = modelChanged || thinkingChanged;
|
|
125645
125827
|
const overflow = alias !== prevModel ? contextOverflowForModel(host.state.appState, alias) : null;
|
|
125646
125828
|
if (overflow !== null) {
|
|
125647
125829
|
host.showNotice("Storm Breaker(风暴守护者)", `无法切换到模型「${alias}」:当前会话上下文 ${formatTokenCount$1(overflow.currentTokens)} 已超出该模型上限 ${formatTokenCount$1(overflow.maxContextTokens)}。建议先执行 /compact 压缩上下文,或选择上下文窗口更大的模型。`);
|
|
@@ -125649,10 +125831,10 @@ async function performModelSwitch(host, alias, thinkingLevel) {
|
|
|
125649
125831
|
}
|
|
125650
125832
|
const session = host.session;
|
|
125651
125833
|
try {
|
|
125652
|
-
if (session === void 0 &&
|
|
125834
|
+
if (session === void 0 && needsSessionActivation) await host.authFlow.activateModelAfterLogin(alias, thinkingLevel);
|
|
125653
125835
|
else if (session !== void 0) {
|
|
125654
|
-
if (
|
|
125655
|
-
if (
|
|
125836
|
+
if (modelChanged) await session.setModel(alias);
|
|
125837
|
+
if (thinkingChanged) await session.setThinking(thinkingLevel);
|
|
125656
125838
|
}
|
|
125657
125839
|
} catch (error) {
|
|
125658
125840
|
const msg = formatErrorMessage(error);
|
|
@@ -125671,7 +125853,12 @@ async function performModelSwitch(host, alias, thinkingLevel) {
|
|
|
125671
125853
|
host.showError(`Switched to ${alias}, but failed to save default: ${msg}`);
|
|
125672
125854
|
return;
|
|
125673
125855
|
}
|
|
125674
|
-
const status =
|
|
125856
|
+
const status = (() => {
|
|
125857
|
+
if (modelChanged) return `Switched to ${alias} with thinking ${thinkingLevel}.`;
|
|
125858
|
+
if (thinkingChanged) return `Thinking set to ${thinkingLevel} for ${alias}.`;
|
|
125859
|
+
if (persisted) return `Saved ${alias} with thinking ${thinkingLevel} as default.`;
|
|
125860
|
+
return `Already using ${alias} with thinking ${thinkingLevel}.`;
|
|
125861
|
+
})();
|
|
125675
125862
|
host.showStatus(status, host.state.theme.colors.success);
|
|
125676
125863
|
}
|
|
125677
125864
|
async function persistModelSelection(host, alias, thinkingLevel) {
|
|
@@ -133020,6 +133207,15 @@ function readImagePath(path) {
|
|
|
133020
133207
|
mimeType: meta.mime
|
|
133021
133208
|
};
|
|
133022
133209
|
}
|
|
133210
|
+
/**
|
|
133211
|
+
* Read an image file from disk into a ClipboardImage. Used when the user
|
|
133212
|
+
* pastes a file URL that the terminal has translated into a text path —
|
|
133213
|
+
* the path points at a real image file, so we load it directly rather
|
|
133214
|
+
* than going through the clipboard native binding.
|
|
133215
|
+
*/
|
|
133216
|
+
function readImageFromPath(path) {
|
|
133217
|
+
return readImagePath(path);
|
|
133218
|
+
}
|
|
133023
133219
|
function readVideoPath(path) {
|
|
133024
133220
|
const mimeType = videoMimeFromPath(path);
|
|
133025
133221
|
if (mimeType === null) return null;
|
|
@@ -133471,6 +133667,16 @@ var EditorKeyboardController = class {
|
|
|
133471
133667
|
return false;
|
|
133472
133668
|
};
|
|
133473
133669
|
editor.onPasteImage = async () => this.handleClipboardImagePaste();
|
|
133670
|
+
editor.onPasteImagePath = async (path) => this.handleImagePathPaste(path);
|
|
133671
|
+
}
|
|
133672
|
+
async handleImagePathPaste(path) {
|
|
133673
|
+
const media = readImageFromPath(path);
|
|
133674
|
+
if (media === null) return;
|
|
133675
|
+
const meta = parseImageMeta(media.bytes);
|
|
133676
|
+
if (meta === null) return;
|
|
133677
|
+
const attachment = this.imageStore.addImage(media.bytes, meta.mime, meta.width, meta.height);
|
|
133678
|
+
this.host.state.editor.insertTextAtCursor?.(`${attachment.placeholder} `);
|
|
133679
|
+
this.host.state.ui.requestRender();
|
|
133474
133680
|
}
|
|
133475
133681
|
clearPendingExit() {
|
|
133476
133682
|
if (!this.pendingExit) return;
|
|
@@ -134413,8 +134619,6 @@ var SessionEventHandler = class {
|
|
|
134413
134619
|
if (event.maxContextTokens !== void 0) patch.maxContextTokens = event.maxContextTokens;
|
|
134414
134620
|
if (event.planMode !== void 0) patch.planMode = event.planMode ? event.planStrategy === "fusion" ? "fusionplan" : this.host.state.appState.planMode === "fusionplan" ? "fusionplan" : "plan" : "off";
|
|
134415
134621
|
if (event.permission !== void 0) patch.permissionMode = event.permission;
|
|
134416
|
-
if (event.model !== void 0) patch.model = event.model;
|
|
134417
|
-
if (event.thinkingLevel !== void 0) patch.thinkingLevel = event.thinkingLevel;
|
|
134418
134622
|
if (Object.keys(patch).length > 0) this.host.setAppState(patch);
|
|
134419
134623
|
}
|
|
134420
134624
|
handleSessionMetaChanged(event) {
|
|
@@ -140734,11 +140938,62 @@ function styleTitle(title, status, colors) {
|
|
|
140734
140938
|
/**
|
|
140735
140939
|
* Custom editor extending pi-tui Editor with app-level keybindings.
|
|
140736
140940
|
*/
|
|
140737
|
-
const ANSI_SGR =
|
|
140941
|
+
const ANSI_SGR = /\[[0-9;]*m/g;
|
|
140738
140942
|
const PASTE_MARKER_RE = /\[paste #(\d+)(?: (?:\+\d+ lines|\d+ chars))?\]/g;
|
|
140739
140943
|
const BRACKET_PASTE_START = "\x1B[200~";
|
|
140740
140944
|
const BRACKET_PASTE_END = "\x1B[201~";
|
|
140741
|
-
const
|
|
140945
|
+
const BRACKETED_IMAGE_PATH_REGEX = /\.(?:png|jpe?g|gif|webp)$/i;
|
|
140946
|
+
const BRACKETED_IMAGE_PATH_BOUNDARY_REGEX = /\.(?:png|jpe?g|gif|webp)(?=$|["']?\s)/gi;
|
|
140947
|
+
const SHELL_ESCAPED_PATH_CHAR_REGEX = /\\([\\\s'"()[\]{}&;<>|?*!$`])/g;
|
|
140948
|
+
function isPastedPathSeparator(char) {
|
|
140949
|
+
return char === void 0 || char === " " || char === " " || char === "\r" || char === "\n";
|
|
140950
|
+
}
|
|
140951
|
+
function imagePathBoundaryEnd(payload, segmentStart, extensionEnd) {
|
|
140952
|
+
const quote = payload[segmentStart];
|
|
140953
|
+
const afterExtension = payload[extensionEnd];
|
|
140954
|
+
if (quote === "\"" || quote === "'") return afterExtension === quote && isPastedPathSeparator(payload[extensionEnd + 1]) ? extensionEnd + 1 : void 0;
|
|
140955
|
+
if (isPastedPathSeparator(afterExtension)) return extensionEnd;
|
|
140956
|
+
}
|
|
140957
|
+
function normalizePastedImagePath(path) {
|
|
140958
|
+
const trimmed = path.trim();
|
|
140959
|
+
const first = trimmed[0];
|
|
140960
|
+
const last = trimmed[trimmed.length - 1];
|
|
140961
|
+
return (trimmed.length > 1 && (first === "\"" || first === "'") && last === first ? trimmed.slice(1, -1) : trimmed).replace(SHELL_ESCAPED_PATH_CHAR_REGEX, "$1");
|
|
140962
|
+
}
|
|
140963
|
+
/**
|
|
140964
|
+
* If a bracketed paste contains only image file paths (one or more,
|
|
140965
|
+
* space/quote separated, all with image extensions), return them. Returns
|
|
140966
|
+
* `undefined` when the paste is mixed text + paths or contains non-image
|
|
140967
|
+
* content — in that case the caller should fall through to normal text
|
|
140968
|
+
* paste. Mirrors oh-my-pi's approach so pasting a Finder-copied image
|
|
140969
|
+
* file becomes a multimodal image attachment instead of a text path the
|
|
140970
|
+
* agent would have to Read manually.
|
|
140971
|
+
*/
|
|
140972
|
+
function extractBracketedImagePastePaths(data) {
|
|
140973
|
+
if (!data.includes(BRACKET_PASTE_START)) return void 0;
|
|
140974
|
+
const startIndex = data.indexOf(BRACKET_PASTE_START);
|
|
140975
|
+
const endIndex = data.indexOf(BRACKET_PASTE_END, startIndex + 6);
|
|
140976
|
+
if (endIndex === -1) return void 0;
|
|
140977
|
+
const pasted = data.slice(startIndex + 6, endIndex).trim();
|
|
140978
|
+
if (pasted.length === 0) return void 0;
|
|
140979
|
+
const paths = [];
|
|
140980
|
+
let segmentStart = 0;
|
|
140981
|
+
BRACKETED_IMAGE_PATH_BOUNDARY_REGEX.lastIndex = 0;
|
|
140982
|
+
for (let match = BRACKETED_IMAGE_PATH_BOUNDARY_REGEX.exec(pasted); match !== null; match = BRACKETED_IMAGE_PATH_BOUNDARY_REGEX.exec(pasted)) {
|
|
140983
|
+
const extensionEnd = match.index + match[0].length;
|
|
140984
|
+
const boundaryEnd = imagePathBoundaryEnd(pasted, segmentStart, extensionEnd);
|
|
140985
|
+
if (boundaryEnd === void 0) continue;
|
|
140986
|
+
const path = normalizePastedImagePath(pasted.slice(segmentStart, boundaryEnd));
|
|
140987
|
+
if (path.length === 0 || !BRACKETED_IMAGE_PATH_REGEX.test(path)) return void 0;
|
|
140988
|
+
paths.push(path);
|
|
140989
|
+
segmentStart = boundaryEnd;
|
|
140990
|
+
while (segmentStart < pasted.length && isPastedPathSeparator(pasted[segmentStart])) segmentStart += 1;
|
|
140991
|
+
BRACKETED_IMAGE_PATH_BOUNDARY_REGEX.lastIndex = segmentStart;
|
|
140992
|
+
}
|
|
140993
|
+
if (paths.length === 0 || segmentStart !== pasted.length) return void 0;
|
|
140994
|
+
return paths;
|
|
140995
|
+
}
|
|
140996
|
+
const KITTY_CSI_U = /^\[(\d+);(\d+)((?::\d+)*)u$/;
|
|
140742
140997
|
const CAPS_LOCK_BIT = 64;
|
|
140743
140998
|
const CTRL_BIT = 4;
|
|
140744
140999
|
const SHIFT_BIT = 1;
|
|
@@ -140770,7 +141025,7 @@ function normalizeCapsLockedCtrl(data) {
|
|
|
140770
141025
|
if (codepoint < 65 || codepoint > 90) return data;
|
|
140771
141026
|
const loweredCodepoint = codepoint + 32;
|
|
140772
141027
|
const strippedModifier = (modifier & -65) + 1;
|
|
140773
|
-
return
|
|
141028
|
+
return `[${String(loweredCodepoint)};${String(strippedModifier)}${tail}u`;
|
|
140774
141029
|
}
|
|
140775
141030
|
/** Convert a visible-char index (ANSI-stripped) back to an index into the raw ANSI-bearing string. */
|
|
140776
141031
|
function mapVisibleIdxToRaw(line, visibleIdx) {
|
|
@@ -140821,6 +141076,15 @@ var CustomEditor = class extends Editor {
|
|
|
140821
141076
|
* the next keystroke.
|
|
140822
141077
|
*/
|
|
140823
141078
|
onPasteImage;
|
|
141079
|
+
/**
|
|
141080
|
+
* Called when a bracketed paste contains only image file paths (e.g.
|
|
141081
|
+
* the user copied an image file in Finder and pasted — the terminal
|
|
141082
|
+
* translates the file URL into a text path). The host loads each path
|
|
141083
|
+
* from disk and registers it as an image attachment, so the paste
|
|
141084
|
+
* becomes a multimodal image input instead of a text path the agent
|
|
141085
|
+
* would have to Read manually.
|
|
141086
|
+
*/
|
|
141087
|
+
onPasteImagePath;
|
|
140824
141088
|
/** Fires exactly once when the user first types anything into the editor. */
|
|
140825
141089
|
onFirstInput;
|
|
140826
141090
|
firstInputFired = false;
|
|
@@ -140912,6 +141176,15 @@ var CustomEditor = class extends Editor {
|
|
|
140912
141176
|
if (!normalized.includes(BRACKET_PASTE_END)) this.consumingPaste = true;
|
|
140913
141177
|
return;
|
|
140914
141178
|
}
|
|
141179
|
+
const pastedImagePaths = extractBracketedImagePastePaths(normalized);
|
|
141180
|
+
if (pastedImagePaths !== void 0 && this.onPasteImagePath !== void 0) {
|
|
141181
|
+
const handler = this.onPasteImagePath;
|
|
141182
|
+
(async () => {
|
|
141183
|
+
for (const path of pastedImagePaths) await handler(path);
|
|
141184
|
+
})();
|
|
141185
|
+
if (!normalized.includes(BRACKET_PASTE_END)) this.consumingPaste = true;
|
|
141186
|
+
return;
|
|
141187
|
+
}
|
|
140915
141188
|
if (matchesKey(normalized, process.platform === "win32" ? "alt+v" : Key.ctrl("v"))) {
|
|
140916
141189
|
if (this.expandPasteMarkerAtCursor()) return;
|
|
140917
141190
|
if (this.onPasteImage !== void 0) {
|
package/dist/main.mjs
CHANGED
|
@@ -6,7 +6,7 @@ const __dirname = __cjsShimDirname(__filename);
|
|
|
6
6
|
import "./suppress-sqlite-warning-C2VB0doZ.mjs";
|
|
7
7
|
//#region src/main.ts
|
|
8
8
|
try {
|
|
9
|
-
(await import("./app-
|
|
9
|
+
(await import("./app-BJoewl4C.mjs")).main();
|
|
10
10
|
} catch (error) {
|
|
11
11
|
process.stderr.write(`${error instanceof Error ? error.stack ?? error.message : String(error)}\n`);
|
|
12
12
|
process.exit(1);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "scream-code",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.10",
|
|
4
4
|
"description": "A terminal-native AI agent for builders",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "ScreamCli",
|
|
@@ -57,10 +57,10 @@
|
|
|
57
57
|
"@types/semver": "^7.7.0",
|
|
58
58
|
"tsx": "^4.21.0",
|
|
59
59
|
"@scream-code/agent-core": "^0.7.0",
|
|
60
|
-
"@scream-code/memory": "0.7.0",
|
|
61
60
|
"@scream-code/config": "^0.7.0",
|
|
62
|
-
"@scream-code/
|
|
63
|
-
"@scream-code/migration-legacy": "^0.7.0"
|
|
61
|
+
"@scream-code/memory": "0.7.0",
|
|
62
|
+
"@scream-code/migration-legacy": "^0.7.0",
|
|
63
|
+
"@scream-code/scream-code-sdk": "^0.7.0"
|
|
64
64
|
},
|
|
65
65
|
"engines": {
|
|
66
66
|
"node": ">=22.19.0"
|