dsh-ops 0.0.0-stage → 0.2.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/CHANGELOG.md +189 -0
- package/LICENSE +30 -0
- package/NOTICE +106 -0
- package/PROVENANCE.md +417 -0
- package/README.en.md +121 -0
- package/README.md +113 -2
- package/README.zh.md +114 -0
- package/bin/dsh-ops.mjs +1216 -0
- package/cordis.patch.yml +160 -0
- package/docs/manual-validation.md +53 -0
- package/docs/schema-baseline.json +64 -0
- package/docs/schema-current.json +84 -0
- package/docs/schema-measurement.md +17 -0
- package/dsh-plugin.json +88 -0
- package/icon.svg +12 -0
- package/lib/binary.js +409 -0
- package/lib/config.js +198 -0
- package/lib/handshake.js +252 -0
- package/lib/index.js +108 -0
- package/lib/jobs.js +42 -0
- package/lib/policy.js +64 -0
- package/lib/presentation.js +63 -0
- package/lib/profile-install.js +61 -0
- package/lib/rust.js +194 -0
- package/lib/session-shells.js +78 -0
- package/lib/shells.js +993 -0
- package/lib/tools.js +657 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +114 -4
- package/vendor/fastctx/Cargo.lock +3210 -0
- package/vendor/fastctx/Cargo.toml +94 -0
- package/vendor/fastctx/FORK.md +119 -0
- package/vendor/fastctx/LICENSE-APACHE +201 -0
- package/vendor/fastctx/NOTICE +40 -0
- package/vendor/fastctx/README.md +439 -0
- package/vendor/fastctx/THIRD_PARTY_LICENSES.md +17 -0
- package/vendor/fastctx/THIRD_PARTY_LICENSES_RUST.md +7914 -0
- package/vendor/fastctx/UPSTREAM.md +49 -0
- package/vendor/fastctx/build.rs +413 -0
- package/vendor/fastctx/src/background_status.rs +403 -0
- package/vendor/fastctx/src/binary.rs +75 -0
- package/vendor/fastctx/src/bounded_sort.rs +500 -0
- package/vendor/fastctx/src/budget.rs +781 -0
- package/vendor/fastctx/src/cli/mod.rs +110 -0
- package/vendor/fastctx/src/context_guard.rs +289 -0
- package/vendor/fastctx/src/control/mod.rs +6 -0
- package/vendor/fastctx/src/control/paths.rs +49 -0
- package/vendor/fastctx/src/control/settings.rs +753 -0
- package/vendor/fastctx/src/control/transaction.rs +531 -0
- package/vendor/fastctx/src/edit/document.rs +535 -0
- package/vendor/fastctx/src/edit/locks.rs +371 -0
- package/vendor/fastctx/src/edit/mod.rs +213 -0
- package/vendor/fastctx/src/edit/private_storage/unix.rs +315 -0
- package/vendor/fastctx/src/edit/private_storage/windows.rs +793 -0
- package/vendor/fastctx/src/edit/private_storage.rs +234 -0
- package/vendor/fastctx/src/edit/replace.rs +1030 -0
- package/vendor/fastctx/src/edit_server.rs +53 -0
- package/vendor/fastctx/src/encoding/reference_v011.rs +587 -0
- package/vendor/fastctx/src/encoding/snapshot_pipeline.rs +1678 -0
- package/vendor/fastctx/src/encoding.rs +1118 -0
- package/vendor/fastctx/src/file_executor.rs +1151 -0
- package/vendor/fastctx/src/file_snapshot.rs +1491 -0
- package/vendor/fastctx/src/glob_filter.rs +98 -0
- package/vendor/fastctx/src/glob_tool.rs +653 -0
- package/vendor/fastctx/src/grep_sink.rs +1162 -0
- package/vendor/fastctx/src/grep_tool.rs +2449 -0
- package/vendor/fastctx/src/lib.rs +45 -0
- package/vendor/fastctx/src/main.rs +15 -0
- package/vendor/fastctx/src/model.rs +51 -0
- package/vendor/fastctx/src/model_guidance.rs +62 -0
- package/vendor/fastctx/src/operation.rs +356 -0
- package/vendor/fastctx/src/ordered_window.rs +1235 -0
- package/vendor/fastctx/src/os_environment.rs +414 -0
- package/vendor/fastctx/src/path_codec.rs +850 -0
- package/vendor/fastctx/src/paths.rs +244 -0
- package/vendor/fastctx/src/process_identity.rs +763 -0
- package/vendor/fastctx/src/process_policy.rs +74 -0
- package/vendor/fastctx/src/read_tool/batch.rs +496 -0
- package/vendor/fastctx/src/read_tool/hex_file.rs +141 -0
- package/vendor/fastctx/src/read_tool/image_file.rs +88 -0
- package/vendor/fastctx/src/read_tool/mod.rs +245 -0
- package/vendor/fastctx/src/read_tool/pdf.rs +470 -0
- package/vendor/fastctx/src/read_tool/pdf_disabled.rs +47 -0
- package/vendor/fastctx/src/read_tool/pdf_engine.rs +664 -0
- package/vendor/fastctx/src/read_tool/text_file.rs +351 -0
- package/vendor/fastctx/src/render_plan.rs +468 -0
- package/vendor/fastctx/src/runtime/activity.rs +159 -0
- package/vendor/fastctx/src/runtime/hosts.rs +99 -0
- package/vendor/fastctx/src/runtime/journal.rs +556 -0
- package/vendor/fastctx/src/runtime/local_ipc.rs +186 -0
- package/vendor/fastctx/src/runtime/mod.rs +746 -0
- package/vendor/fastctx/src/runtime/protocol.rs +296 -0
- package/vendor/fastctx/src/runtime/session.rs +536 -0
- package/vendor/fastctx/src/runtime/windows_process.rs +66 -0
- package/vendor/fastctx/src/search_parallelism.rs +106 -0
- package/vendor/fastctx/src/search_text.rs +227 -0
- package/vendor/fastctx/src/server.rs +359 -0
- package/vendor/fastctx/src/server_manifest.rs +468 -0
- package/vendor/fastctx/src/server_support.rs +826 -0
- package/vendor/fastctx/src/session.rs +629 -0
- package/vendor/fastctx/src/shell/apply_patch_hint.rs +41 -0
- package/vendor/fastctx/src/shell/bash.rs +263 -0
- package/vendor/fastctx/src/shell/buffer.rs +108 -0
- package/vendor/fastctx/src/shell/encoding.rs +403 -0
- package/vendor/fastctx/src/shell/foreground.rs +115 -0
- package/vendor/fastctx/src/shell/jobs/admission.rs +91 -0
- package/vendor/fastctx/src/shell/jobs/background.rs +146 -0
- package/vendor/fastctx/src/shell/jobs/host.rs +830 -0
- package/vendor/fastctx/src/shell/jobs/identity.rs +29 -0
- package/vendor/fastctx/src/shell/jobs/mod.rs +1513 -0
- package/vendor/fastctx/src/shell/jobs/model.rs +244 -0
- package/vendor/fastctx/src/shell/jobs/output_log.rs +1148 -0
- package/vendor/fastctx/src/shell/jobs/store.rs +1300 -0
- package/vendor/fastctx/src/shell/mod.rs +345 -0
- package/vendor/fastctx/src/shell/normalize.rs +389 -0
- package/vendor/fastctx/src/shell/output.rs +406 -0
- package/vendor/fastctx/src/shell/process.rs +493 -0
- package/vendor/fastctx/src/shell_server.rs +156 -0
- package/vendor/fastctx/src/skip_report.rs +83 -0
- package/vendor/fastctx/src/stdio_transport.rs +177 -0
- package/vendor/fastctx/src/tool_schema.rs +204 -0
- package/vendor/fastctx/src/traversal.rs +846 -0
- package/vendor/fastctx/third-party/pdfium-7763/LICENSE +9 -0
- package/vendor/fastctx/third-party/pdfium-7763/licenses/abseil.txt +202 -0
- package/vendor/fastctx/third-party/pdfium-7763/licenses/agg23.txt +14 -0
- package/vendor/fastctx/third-party/pdfium-7763/licenses/fast_float.txt +27 -0
- package/vendor/fastctx/third-party/pdfium-7763/licenses/freetype.txt +169 -0
- package/vendor/fastctx/third-party/pdfium-7763/licenses/icu.txt +542 -0
- package/vendor/fastctx/third-party/pdfium-7763/licenses/lcms.txt +27 -0
- package/vendor/fastctx/third-party/pdfium-7763/licenses/libjpeg_turbo.ijg +260 -0
- package/vendor/fastctx/third-party/pdfium-7763/licenses/libjpeg_turbo.md +135 -0
- package/vendor/fastctx/third-party/pdfium-7763/licenses/libopenjpeg.txt +32 -0
- package/vendor/fastctx/third-party/pdfium-7763/licenses/libpng.txt +134 -0
- package/vendor/fastctx/third-party/pdfium-7763/licenses/libtiff.txt +21 -0
- package/vendor/fastctx/third-party/pdfium-7763/licenses/llvm-libc.txt +278 -0
- package/vendor/fastctx/third-party/pdfium-7763/licenses/pdfium.txt +230 -0
- package/vendor/fastctx/third-party/pdfium-7763/licenses/simdutf.txt +18 -0
- package/vendor/fastctx/third-party/pdfium-7763/licenses/zlib.txt +29 -0
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
//! Shell output capture, bounded presentation windows, and terminal status notes.
|
|
2
|
+
|
|
3
|
+
use crate::budget::{
|
|
4
|
+
JOB_OUTPUT_TOKEN_BUDGET_ENV, RUN_TOKEN_BUDGET_ENV, TokenBudget, estimate_tokens, token_budget,
|
|
5
|
+
tool_token_budget, tool_token_budget_for_required,
|
|
6
|
+
};
|
|
7
|
+
use crate::model::ToolResponse;
|
|
8
|
+
use crate::shell::apply_patch_hint;
|
|
9
|
+
use crate::shell::buffer::{BufferedLine, LineRing};
|
|
10
|
+
use crate::shell::encoding::{EncodedLine, OutputEncoding, decode_run, run_garble_note};
|
|
11
|
+
use crate::shell::normalize::StreamNormalizer;
|
|
12
|
+
use std::io::Read;
|
|
13
|
+
|
|
14
|
+
/// A normalized output stream retained in an eight-megabyte whole-line ring.
|
|
15
|
+
#[derive(Clone, Debug, Default, Eq, PartialEq)]
|
|
16
|
+
pub(crate) struct CapturedOutput {
|
|
17
|
+
ring: LineRing,
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
impl CapturedOutput {
|
|
21
|
+
pub(crate) fn total_lines(&self) -> u64 {
|
|
22
|
+
self.ring.total_lines()
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
pub(crate) fn retained_lines(&self) -> Vec<BufferedLine> {
|
|
26
|
+
self.ring.all()
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
pub(crate) fn dropped_lines(&self) -> u64 {
|
|
30
|
+
self.ring.dropped_lines()
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/// Reads and normalizes the merged process pipe through EOF without controlling process life.
|
|
35
|
+
pub(crate) fn capture_foreground(mut reader: impl Read) -> std::io::Result<CapturedOutput> {
|
|
36
|
+
let mut normalizer = StreamNormalizer::new();
|
|
37
|
+
let mut output = CapturedOutput::default();
|
|
38
|
+
let mut buffer = [0_u8; 16 * 1024];
|
|
39
|
+
loop {
|
|
40
|
+
let read = reader.read(&mut buffer)?;
|
|
41
|
+
if read == 0 {
|
|
42
|
+
break;
|
|
43
|
+
}
|
|
44
|
+
let mut lines = Vec::new();
|
|
45
|
+
normalizer.push(&buffer[..read], &mut lines);
|
|
46
|
+
for line in lines {
|
|
47
|
+
output.ring.push(line);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
let mut lines = Vec::new();
|
|
51
|
+
normalizer.finish(&mut lines);
|
|
52
|
+
for line in lines {
|
|
53
|
+
output.ring.push(line);
|
|
54
|
+
}
|
|
55
|
+
Ok(output)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/// Rejects an unusably small run budget before a command can cause side effects.
|
|
59
|
+
pub(crate) fn validate_run_budget(timeout_ms: u64) -> Result<TokenBudget, String> {
|
|
60
|
+
let maximum = u64::MAX;
|
|
61
|
+
let drop_note = dropped_note(maximum).expect("a positive count always creates a note");
|
|
62
|
+
let ring_loss_terminal = window_terminal(i32::MIN, None, 0, 0, maximum);
|
|
63
|
+
let ring_loss = compose_response(
|
|
64
|
+
Some(&drop_note),
|
|
65
|
+
&[format!("... [{maximum} lines omitted] ...")],
|
|
66
|
+
&ring_loss_terminal,
|
|
67
|
+
);
|
|
68
|
+
let candidates = [
|
|
69
|
+
format!("(Complete: exited {}; no output.)", i32::MIN),
|
|
70
|
+
format!("(Complete: exited {}; {maximum} lines.)", i32::MIN),
|
|
71
|
+
format!(
|
|
72
|
+
"(Partial: exited {}; {maximum} lines shown, but one or more long lines were truncated at 2000 chars.)",
|
|
73
|
+
i32::MIN
|
|
74
|
+
),
|
|
75
|
+
format!(
|
|
76
|
+
"(Partial: showing the first 0 and last 0 of {maximum} lines; exited {}.)",
|
|
77
|
+
i32::MIN
|
|
78
|
+
),
|
|
79
|
+
format!(
|
|
80
|
+
"(Killed: timed out after {timeout_ms} ms and the process tree was killed; no output captured. Re-run with a larger timeout_ms or use run_background.)"
|
|
81
|
+
),
|
|
82
|
+
format!(
|
|
83
|
+
"(Killed: timed out after {timeout_ms} ms and the process tree was killed; {maximum} lines captured. Re-run with a larger timeout_ms or use run_background.)"
|
|
84
|
+
),
|
|
85
|
+
format!(
|
|
86
|
+
"(Killed: timed out after {timeout_ms} ms and the process tree was killed; showing the first 0 and last 0 of {maximum} captured lines. Re-run with a larger timeout_ms or use run_background.)"
|
|
87
|
+
),
|
|
88
|
+
ring_loss,
|
|
89
|
+
];
|
|
90
|
+
let required = candidates
|
|
91
|
+
.iter()
|
|
92
|
+
.map(|candidate| estimate_tokens(candidate))
|
|
93
|
+
.max()
|
|
94
|
+
.unwrap_or(0);
|
|
95
|
+
let budget = tool_token_budget_for_required(RUN_TOKEN_BUDGET_ENV, required)?;
|
|
96
|
+
if required <= budget.value {
|
|
97
|
+
Ok(budget)
|
|
98
|
+
} else {
|
|
99
|
+
Err(budget_too_small_message(budget))
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
pub(crate) fn run_token_budget() -> Result<TokenBudget, String> {
|
|
104
|
+
tool_token_budget(RUN_TOKEN_BUDGET_ENV)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
pub(crate) fn job_output_token_budget() -> Result<TokenBudget, String> {
|
|
108
|
+
tool_token_budget(JOB_OUTPUT_TOKEN_BUDGET_ENV)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
pub(crate) fn global_token_budget() -> Result<TokenBudget, String> {
|
|
112
|
+
token_budget().map(|value| TokenBudget {
|
|
113
|
+
value,
|
|
114
|
+
variable: "FASTCTX_TOKEN_BUDGET",
|
|
115
|
+
})
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
pub(crate) fn budget_too_small_message(budget: TokenBudget) -> String {
|
|
119
|
+
format!(
|
|
120
|
+
"{}={} is too small to return the required status note. Increase it and retry.",
|
|
121
|
+
budget.variable, budget.value
|
|
122
|
+
)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
pub(crate) fn terminal_response(terminal: String, budget: TokenBudget) -> ToolResponse {
|
|
126
|
+
let required = estimate_tokens(&terminal);
|
|
127
|
+
let budget = tool_token_budget_for_required(budget.variable, required).unwrap_or(budget);
|
|
128
|
+
if required <= budget.value {
|
|
129
|
+
ToolResponse::text(terminal)
|
|
130
|
+
} else {
|
|
131
|
+
ToolResponse::error(budget_too_small_message(budget))
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/// Formats a normal or timed-out foreground result without writing any shell artifacts.
|
|
136
|
+
pub(crate) fn format_foreground(
|
|
137
|
+
output: &CapturedOutput,
|
|
138
|
+
command: &str,
|
|
139
|
+
exit_code: i32,
|
|
140
|
+
timeout_ms: Option<u64>,
|
|
141
|
+
encoding: Option<OutputEncoding>,
|
|
142
|
+
) -> ToolResponse {
|
|
143
|
+
let budget = match run_token_budget() {
|
|
144
|
+
Ok(budget) => budget,
|
|
145
|
+
Err(error) => return ToolResponse::error(error),
|
|
146
|
+
};
|
|
147
|
+
format_foreground_with_budget(output, command, exit_code, timeout_ms, encoding, budget)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
fn format_foreground_with_budget(
|
|
151
|
+
output: &CapturedOutput,
|
|
152
|
+
command: &str,
|
|
153
|
+
exit_code: i32,
|
|
154
|
+
timeout_ms: Option<u64>,
|
|
155
|
+
encoding: Option<OutputEncoding>,
|
|
156
|
+
budget: TokenBudget,
|
|
157
|
+
) -> ToolResponse {
|
|
158
|
+
let retained = output.retained_lines();
|
|
159
|
+
let encoded = retained
|
|
160
|
+
.iter()
|
|
161
|
+
.map(|line| EncodedLine {
|
|
162
|
+
bytes: &line.bytes,
|
|
163
|
+
total_bytes: line.total_bytes,
|
|
164
|
+
stream_encoding: line.stream_encoding,
|
|
165
|
+
legacy_text: None,
|
|
166
|
+
known_truncated: line.raw_truncated,
|
|
167
|
+
})
|
|
168
|
+
.collect::<Vec<_>>();
|
|
169
|
+
let decoded = decode_run(&encoded, encoding);
|
|
170
|
+
let lines = decoded.lines;
|
|
171
|
+
let total = output.total_lines();
|
|
172
|
+
let dropped = output.dropped_lines();
|
|
173
|
+
let trailing = join_notes(
|
|
174
|
+
decoded.transcoding_note.as_deref(),
|
|
175
|
+
apply_patch_hint::misuse_note(command, exit_code, timeout_ms).as_deref(),
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
if dropped == 0 {
|
|
179
|
+
let terminal = full_terminal(exit_code, timeout_ms, total, decoded.had_truncation);
|
|
180
|
+
let leading = run_garble_note(decoded.invalid_sequences);
|
|
181
|
+
let response =
|
|
182
|
+
compose_response_with_tail(leading.as_deref(), &lines, trailing.as_deref(), &terminal);
|
|
183
|
+
if estimate_tokens(&response) <= budget.value {
|
|
184
|
+
return ToolResponse::text(response);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
let window = ForegroundWindow {
|
|
189
|
+
lines: &lines,
|
|
190
|
+
invalid_per_line: &decoded.invalid_sequences_per_line,
|
|
191
|
+
trailing_notes: trailing.as_deref(),
|
|
192
|
+
total,
|
|
193
|
+
dropped,
|
|
194
|
+
exit_code,
|
|
195
|
+
timeout_ms,
|
|
196
|
+
};
|
|
197
|
+
match largest_head_tail_that_fits(&window, budget.value) {
|
|
198
|
+
Some(response) => ToolResponse::text(response),
|
|
199
|
+
None => ToolResponse::error(budget_too_small_message(budget)),
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
fn full_terminal(
|
|
204
|
+
exit_code: i32,
|
|
205
|
+
timeout_ms: Option<u64>,
|
|
206
|
+
total: u64,
|
|
207
|
+
had_truncation: bool,
|
|
208
|
+
) -> String {
|
|
209
|
+
match timeout_ms {
|
|
210
|
+
Some(timeout) if total == 0 => format!(
|
|
211
|
+
"(Killed: timed out after {timeout} ms and the process tree was killed; no output captured. Re-run with a larger timeout_ms or use run_background.)"
|
|
212
|
+
),
|
|
213
|
+
Some(timeout) => format!(
|
|
214
|
+
"(Killed: timed out after {timeout} ms and the process tree was killed; {total} {} captured. Re-run with a larger timeout_ms or use run_background.)",
|
|
215
|
+
plural(total, "line", "lines")
|
|
216
|
+
),
|
|
217
|
+
None if total == 0 => format!("(Complete: exited {exit_code}; no output.)"),
|
|
218
|
+
None if had_truncation => format!(
|
|
219
|
+
"(Partial: exited {exit_code}; {total} {} shown, but one or more long lines were truncated at 2000 chars.)",
|
|
220
|
+
plural(total, "line", "lines")
|
|
221
|
+
),
|
|
222
|
+
None => format!(
|
|
223
|
+
"(Complete: exited {exit_code}; {total} {}.)",
|
|
224
|
+
plural(total, "line", "lines")
|
|
225
|
+
),
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
fn window_terminal(
|
|
230
|
+
exit_code: i32,
|
|
231
|
+
timeout_ms: Option<u64>,
|
|
232
|
+
first: usize,
|
|
233
|
+
last: usize,
|
|
234
|
+
total: u64,
|
|
235
|
+
) -> String {
|
|
236
|
+
match timeout_ms {
|
|
237
|
+
None => format!(
|
|
238
|
+
"(Partial: showing the first {first} and last {last} of {total} lines; exited {exit_code}.)"
|
|
239
|
+
),
|
|
240
|
+
Some(timeout) => format!(
|
|
241
|
+
"(Killed: timed out after {timeout} ms and the process tree was killed; showing the first {first} and last {last} of {total} captured lines. Re-run with a larger timeout_ms or use run_background.)"
|
|
242
|
+
),
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
struct ForegroundWindow<'a> {
|
|
247
|
+
lines: &'a [String],
|
|
248
|
+
invalid_per_line: &'a [u64],
|
|
249
|
+
/// Already-joined notes that sit between the output and the terminal note.
|
|
250
|
+
trailing_notes: Option<&'a str>,
|
|
251
|
+
total: u64,
|
|
252
|
+
dropped: u64,
|
|
253
|
+
exit_code: i32,
|
|
254
|
+
timeout_ms: Option<u64>,
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
#[derive(Clone, Copy)]
|
|
258
|
+
struct WindowBounds {
|
|
259
|
+
first: usize,
|
|
260
|
+
last: usize,
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
fn largest_head_tail_that_fits(window: &ForegroundWindow<'_>, budget: usize) -> Option<String> {
|
|
264
|
+
let base = window_candidate(window, WindowBounds { first: 0, last: 0 });
|
|
265
|
+
let base_tokens = estimate_tokens(&base);
|
|
266
|
+
if base_tokens > budget {
|
|
267
|
+
return None;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
let head_target = budget.saturating_sub(base_tokens) / 10;
|
|
271
|
+
let first = largest_prefix_within(window.lines, head_target);
|
|
272
|
+
let remaining = window.lines.len().saturating_sub(first);
|
|
273
|
+
|
|
274
|
+
let mut low = 0;
|
|
275
|
+
let mut high = remaining;
|
|
276
|
+
let mut best = base;
|
|
277
|
+
while low <= high {
|
|
278
|
+
let last = low + (high - low) / 2;
|
|
279
|
+
let candidate = window_candidate(window, WindowBounds { first, last });
|
|
280
|
+
if estimate_tokens(&candidate) <= budget {
|
|
281
|
+
best = candidate;
|
|
282
|
+
low = last.saturating_add(1);
|
|
283
|
+
} else if last == 0 {
|
|
284
|
+
break;
|
|
285
|
+
} else {
|
|
286
|
+
high = last - 1;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
Some(best)
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
fn largest_prefix_within(lines: &[String], token_target: usize) -> usize {
|
|
293
|
+
if token_target == 0 {
|
|
294
|
+
return 0;
|
|
295
|
+
}
|
|
296
|
+
let mut low = 0;
|
|
297
|
+
let mut high = lines.len();
|
|
298
|
+
let mut best = 0;
|
|
299
|
+
while low <= high {
|
|
300
|
+
let middle = low + (high - low) / 2;
|
|
301
|
+
let tokens = estimate_tokens(&lines[..middle].join("\n"));
|
|
302
|
+
if tokens <= token_target {
|
|
303
|
+
best = middle;
|
|
304
|
+
low = middle.saturating_add(1);
|
|
305
|
+
} else if middle == 0 {
|
|
306
|
+
break;
|
|
307
|
+
} else {
|
|
308
|
+
high = middle - 1;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
best
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
fn window_candidate(window: &ForegroundWindow<'_>, bounds: WindowBounds) -> String {
|
|
315
|
+
let WindowBounds { first, last } = bounds;
|
|
316
|
+
let mut body = Vec::with_capacity(first.saturating_add(last).saturating_add(1));
|
|
317
|
+
body.extend(window.lines.iter().take(first).cloned());
|
|
318
|
+
let omitted = window
|
|
319
|
+
.total
|
|
320
|
+
.saturating_sub(first.saturating_add(last) as u64);
|
|
321
|
+
if omitted > 0 {
|
|
322
|
+
body.push(format!("... [{omitted} lines omitted] ..."));
|
|
323
|
+
}
|
|
324
|
+
if last > 0 {
|
|
325
|
+
body.extend(window.lines[window.lines.len() - last..].iter().cloned());
|
|
326
|
+
}
|
|
327
|
+
let invalid = window
|
|
328
|
+
.invalid_per_line
|
|
329
|
+
.iter()
|
|
330
|
+
.take(first)
|
|
331
|
+
.chain(window.invalid_per_line.iter().rev().take(last))
|
|
332
|
+
.copied()
|
|
333
|
+
.fold(0_u64, u64::saturating_add);
|
|
334
|
+
let garble_note = run_garble_note(invalid);
|
|
335
|
+
let drop_note = dropped_note(window.dropped);
|
|
336
|
+
let leading = match (drop_note.as_deref(), garble_note.as_deref()) {
|
|
337
|
+
(Some(drop_note), Some(garble_note)) => Some(format!("{drop_note}\n\n{garble_note}")),
|
|
338
|
+
(Some(drop_note), None) => Some(drop_note.to_string()),
|
|
339
|
+
(None, Some(garble_note)) => Some(garble_note.to_string()),
|
|
340
|
+
(None, None) => None,
|
|
341
|
+
};
|
|
342
|
+
let terminal = window_terminal(
|
|
343
|
+
window.exit_code,
|
|
344
|
+
window.timeout_ms,
|
|
345
|
+
first,
|
|
346
|
+
last,
|
|
347
|
+
window.total,
|
|
348
|
+
);
|
|
349
|
+
compose_response_with_tail(leading.as_deref(), &body, window.trailing_notes, &terminal)
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/// Joins two optional notes with the same separator `compose_response_with_tail` uses between them.
|
|
353
|
+
fn join_notes(first: Option<&str>, second: Option<&str>) -> Option<String> {
|
|
354
|
+
match (first, second) {
|
|
355
|
+
(Some(first), Some(second)) => Some(format!("{first}\n{second}")),
|
|
356
|
+
(Some(only), None) | (None, Some(only)) => Some(only.to_string()),
|
|
357
|
+
(None, None) => None,
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/// Reports ring-buffer loss as a bare fact: what to do about it is the caller's
|
|
362
|
+
/// call, and the only remedy would be re-running a command whose sheer output
|
|
363
|
+
/// volume makes heavy side effects likely (2026-07-25).
|
|
364
|
+
pub(crate) fn dropped_note(dropped: u64) -> Option<String> {
|
|
365
|
+
(dropped > 0).then(|| {
|
|
366
|
+
format!(
|
|
367
|
+
"(Note: {dropped} earlier {} {} dropped from the buffer and cannot be retrieved.)",
|
|
368
|
+
plural(dropped, "line", "lines"),
|
|
369
|
+
if dropped == 1 { "was" } else { "were" }
|
|
370
|
+
)
|
|
371
|
+
})
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
pub(crate) fn compose_response(
|
|
375
|
+
leading_note: Option<&str>,
|
|
376
|
+
lines: &[String],
|
|
377
|
+
terminal: &str,
|
|
378
|
+
) -> String {
|
|
379
|
+
compose_response_with_tail(leading_note, lines, None, terminal)
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
pub(crate) fn compose_response_with_tail(
|
|
383
|
+
leading_note: Option<&str>,
|
|
384
|
+
lines: &[String],
|
|
385
|
+
trailing_note: Option<&str>,
|
|
386
|
+
terminal: &str,
|
|
387
|
+
) -> String {
|
|
388
|
+
let mut notes = Vec::with_capacity(2);
|
|
389
|
+
if let Some(note) = trailing_note {
|
|
390
|
+
notes.push(note.to_string());
|
|
391
|
+
}
|
|
392
|
+
notes.push(terminal.to_string());
|
|
393
|
+
let body = if lines.is_empty() {
|
|
394
|
+
notes.join("\n")
|
|
395
|
+
} else {
|
|
396
|
+
format!("{}\n\n{}", lines.join("\n"), notes.join("\n"))
|
|
397
|
+
};
|
|
398
|
+
match leading_note {
|
|
399
|
+
Some(note) => format!("{note}\n\n{body}"),
|
|
400
|
+
None => body,
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
pub(crate) fn plural<'a>(count: u64, singular: &'a str, plural: &'a str) -> &'a str {
|
|
405
|
+
if count == 1 { singular } else { plural }
|
|
406
|
+
}
|