Merge pull request #6101 from nesquena/stage/6062-readrange
Release exp: show requested line range in collapsed read_file tool labels (#6062)
This commit is contained in:
5 files changed
+61
-4
No files matched your search
@@ -5,6 +5,8 @@
|
||||
|
||||
### Added
|
||||
|
||||
- **Collapsed `read_file` tool rows now show the requested line range.** When the agent reads a slice of a file (`read_file` with `offset`/`limit`), the collapsed tool label now appends the range — e.g. `Read hermes_constants.py · L321-480`, or `L42` for an offset-only read — so you can see which part of a large file was read without expanding the row. Full-file reads and any tool call without a trustworthy positive-integer range keep the plain filename label (invalid, non-integer, non-positive, and overflowing ranges all fall back safely). Only `read_file` is affected. Thanks @franksong2702. (#6062)
|
||||
|
||||
- **Images and media now render inline *as the response streams*, not just after it finishes.** When the assistant emits a `MEDIA:` token (generated image, video, audio, PDF, diff, CSV, Excalidraw, etc.) mid-turn, it now appears inline in real time during streaming instead of only materializing once the turn settles — including markers that arrive split across streaming chunks. Media routes through the same escaped `/api/media` path (no XSS surface: `javascript:`/`data:`/`file:` neutralized, SVG stays an `<img>`), remote `.webm` correctly renders as video, and normal prose streaming (including the fade effect) is unchanged when no media is present. Thanks @silent-reader-cn. (#5802)
|
||||
|
||||
- **Docker / no-git installs now get an update notice.** Container and other non-git WebUI installs previously had no way to see that a newer release existed (the in-app updater only works on git checkouts). The Settings update panel now shows an informational notice when the running image trails the latest published release — e.g. "WebUI (v0.52.30 → v0.52.43): 13 releases available · Manual update required: run `docker pull …`, then recreate the container." It is informational only: the Force-update and Clear-lock actions are disabled/hidden for these installs and the notice can never trigger a git reset. Prerelease tags are ignored so stable installs only see stable releases. Thanks @rodboev. (#5733, #2729)
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 51 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 47 KiB |
+21
-2
@@ -10738,7 +10738,7 @@ function _transparentToolSummary(tc){
|
||||
// with only {workdir} or an unknown tool with {mode:"dry-run"}) must yield an
|
||||
// EMPTY collapsed preview rather than dumping a raw arg snippet — that keeps the
|
||||
// collapsed row quiet and consistent with the no-args case (#4658 review).
|
||||
const target=typeof _toolVisibleTargetLabel==='function'?_toolVisibleTargetLabel(tc,{limit:160}):'';
|
||||
const target=typeof _toolVisibleTargetLabel==='function'?_toolVisibleTargetLabel(tc,{limit:160,rangeFirst:true}):'';
|
||||
if(target) return target;
|
||||
return '';
|
||||
}
|
||||
@@ -16719,6 +16719,20 @@ function _toolTargetLabel(tc){
|
||||
else raw=a.cmd||a.command||a.path||a.file_path||a.file||a.uri||a.url||a.query||a.pattern||a.dir||a.task||a.name||'';
|
||||
return _redactToolTargetLabel(_decodeToolLabelEntities(String(raw).split('\n')[0].trim()));
|
||||
}
|
||||
function _toolReadRangeLabel(tc){
|
||||
const name=String(tc&&tc.name||'').toLowerCase().replace(/[^a-z0-9]+/g,'_');
|
||||
if(name!=='read_file') return '';
|
||||
const args=tc&&tc.args||{};
|
||||
const offset=args.offset;
|
||||
if(!Number.isSafeInteger(offset)||offset<=0) return '';
|
||||
const limit=args.limit;
|
||||
if(limit===undefined) return `L${offset}`;
|
||||
if(!Number.isSafeInteger(limit)||limit<=0) return '';
|
||||
if(limit===1) return `L${offset}`;
|
||||
const span=limit-1;
|
||||
if(offset>Number.MAX_SAFE_INTEGER-span) return '';
|
||||
return `L${offset}-${offset+span}`;
|
||||
}
|
||||
function _toolFullCommandLabel(tc){
|
||||
// Full (multi-line) shell command for the EXPANDED detail lead. Mirrors the
|
||||
// shell raw-extraction in _toolTargetLabel but WITHOUT the .split('\n')[0]
|
||||
@@ -16733,7 +16747,12 @@ function _toolVisibleTargetLabel(tc, opts){
|
||||
const target=_toolTargetLabel(tc);
|
||||
if(!target) return '';
|
||||
const kind=_toolActionKind(tc);
|
||||
if(kind==='read'||kind==='write') return _shortToolLabel(_toolPathBasename(target)||target, opts.limit||112);
|
||||
if(kind==='read'||kind==='write'){
|
||||
let text=_toolPathBasename(target)||target;
|
||||
const range=kind==='read'?_toolReadRangeLabel(tc):'';
|
||||
if(range) text=opts.rangeFirst?`${range} · ${text}`:`${text} · ${range}`;
|
||||
return _shortToolLabel(text, opts.limit||112);
|
||||
}
|
||||
if(kind==='skill'){
|
||||
const suffix=_toolI18n('tool_target_skill_suffix', 'skill');
|
||||
const text=target.toLowerCase().endsWith(String(suffix).toLowerCase())?target:`${target} ${suffix}`;
|
||||
|
||||
@@ -39,7 +39,7 @@ pytestmark = pytest.mark.skipif(NODE is None, reason="node not on PATH")
|
||||
_FN_NAMES = [
|
||||
'_toolDisplayName', '_toolActionKind', '_toolKindIcon', '_toolPathBasename',
|
||||
'_decodeToolLabelEntities', '_redactToolTargetLabel', '_shortToolLabel', '_toolI18n',
|
||||
'_toolTargetLabel', '_toolVisibleTargetLabel', '_toolCommandTitle', '_toolQueryTitle',
|
||||
'_toolTargetLabel', '_toolReadRangeLabel', '_toolVisibleTargetLabel', '_toolCommandTitle', '_toolQueryTitle',
|
||||
'_toolActionLabelText', '_toolArgPreviewValue', '_toolArgPreviewKeyIsHidden',
|
||||
'_formatToolArgPreview', '_toolResultOneLiner', '_toolCardPreviewText', '_toolCardAllowsDetail',
|
||||
'_toolDetailLeadLabel', '_toolDetailLeadText', '_toolShortName', '_transparentEventPreview',
|
||||
@@ -178,7 +178,11 @@ function previewFor(tc){
|
||||
const header=card?card.querySelector('.tool-card-header'):null;
|
||||
const preview=header?header.querySelector('.tool-card-preview'):null;
|
||||
const name=header?header.querySelector('.tool-card-name'):null;
|
||||
return { preview: preview?preview.textContent:null, name: name?name.textContent:null };
|
||||
return {
|
||||
preview: preview?preview.textContent:null,
|
||||
name: name?name.textContent:null,
|
||||
actionLabel: row.dataset.toolActionLabel||null,
|
||||
};
|
||||
}
|
||||
|
||||
const cases = JSON.parse(process.argv[2]);
|
||||
@@ -190,6 +194,13 @@ process.stdout.write(JSON.stringify(out));
|
||||
|
||||
CASES = {
|
||||
"read_file": {"name": "read_file", "args": {"path": "/home/x/api/config.py"}, "snippet": '{"content":"1|import os"}', "done": True},
|
||||
"read_file_range": {"name": "read_file", "args": {"path": "/home/x/api/hermes_constants.py", "offset": 321, "limit": 160}, "done": True},
|
||||
"read_file_offset_only": {"name": "read_file", "args": {"path": "/home/x/api/hermes_constants.py", "offset": 42}, "done": True},
|
||||
"read_file_invalid_range": {"name": "read_file", "args": {"path": "/home/x/api/hermes_constants.py", "offset": "321", "limit": 160}, "done": True},
|
||||
"read_file_invalid_limit": {"name": "read_file", "args": {"path": "/home/x/api/hermes_constants.py", "offset": 321, "limit": 0}, "done": True},
|
||||
"read_file_negative_limit": {"name": "read_file", "args": {"path": "/home/x/api/hermes_constants.py", "offset": 321, "limit": -1}, "done": True},
|
||||
"read_file_null_limit": {"name": "read_file", "args": {"path": "/home/x/api/hermes_constants.py", "offset": 321, "limit": None}, "done": True},
|
||||
"read_file_overflow_range": {"name": "read_file", "args": {"path": "/home/x/api/hermes_constants.py", "offset": 9007199254740991, "limit": 2}, "done": True},
|
||||
"terminal": {"name": "terminal", "args": {"command": "git fetch origin --quiet"}, "snippet": "275 /opt/...", "done": True},
|
||||
"search_files": {"name": "search_files", "args": {"pattern": "buildToolCard", "path": "/tmp"}, "snippet": '{"total_count": 0}', "done": True},
|
||||
"skill_view": {"name": "skill_view", "args": {"name": "opencode-review-agents"}, "snippet": '{"success": true}', "done": True},
|
||||
@@ -226,6 +237,31 @@ def test_read_file_row_shows_file_target(results):
|
||||
)
|
||||
|
||||
|
||||
def test_read_file_row_shows_requested_line_range(results):
|
||||
r = results["read_file_range"]
|
||||
assert r["preview"] == "L321-480 · hermes_constants.py"
|
||||
assert r["actionLabel"] == "Read hermes_constants.py · L321-480"
|
||||
|
||||
|
||||
def test_read_file_offset_without_limit_shows_start_line(results):
|
||||
r = results["read_file_offset_only"]
|
||||
assert r["preview"] == "L42 · hermes_constants.py"
|
||||
assert r["actionLabel"] == "Read hermes_constants.py · L42"
|
||||
|
||||
|
||||
def test_read_file_without_trustworthy_numeric_range_keeps_old_label(results):
|
||||
no_range = results["read_file"]
|
||||
invalid_range = results["read_file_invalid_range"]
|
||||
assert no_range["preview"] == "config.py"
|
||||
assert no_range["actionLabel"] == "Read config.py"
|
||||
assert invalid_range["preview"] == "hermes_constants.py"
|
||||
assert invalid_range["actionLabel"] == "Read hermes_constants.py"
|
||||
for key in ("read_file_invalid_limit", "read_file_negative_limit", "read_file_null_limit", "read_file_overflow_range"):
|
||||
invalid_limit = results[key]
|
||||
assert invalid_limit["preview"] == "hermes_constants.py"
|
||||
assert invalid_limit["actionLabel"] == "Read hermes_constants.py"
|
||||
|
||||
|
||||
def test_terminal_row_shows_command(results):
|
||||
r = results["terminal"]
|
||||
assert r["preview"], f"collapsed terminal row must not be blank (the #4658 regression): {r}"
|
||||
|
||||
Reference in new issue
Block a user