Files
llama.cpp/tools/ui/src/lib/utils/path-display.ts
Aleksander Grygier 2f56fc3431 ui: CWD for agent (#26518)
* server : extend file_glob_search for UI pickers

* ui : add per-conversation working directory with picker

* ui : add path navigation and search scope to cwd picker

Treat path-like queries (starting with / or ~) as directory navigation
instead of glob-matching the whole query: search the parent for the last
segment, and descend into an exactly-typed directory by listing its
children. Show the effective search scope in the footer and auto-search
on open so the current directory and its siblings appear immediately.

Assisted-by: Claude

* db : persist per-call tool cwd on tool result messages

* ui : abbreviate tool paths under home with a tilde

* ui : show the per-call cwd on exec shell rows

* ui : clarify the synthetic cwd message for the model

* ui : reuse the trailing cwd row on a repeated pick

* ui : don't jump when a cwd row is injected mid-chat

* chore: Formatting

* refactor: Cleanup comments

* ui : unify working directory naming and add a synthetic-message flag

* ui : render synthetic cwd rows without a scroll jump

* ui : decouple the working directory picker into utils and sub-components

* ui : add get_info tool call block

* chore: Formatting

* refactor: Cleanup

* refactor: Cleanup

* refactor: Cleanup

* fix: UI

* server : harden file_glob_search listing (kind enum, timeout, symlink guard, absolute base)

* ui : use persisted isSynthetic flag for cwd rows, drop legacy formats

* ui : cache picker search, fail visibly on native resolve

* ui : escape glob metacharacters in picker search glob

* ui : simplify auto-scroll pin

* chore: Format

* fix: Use `SvelteMap`

* refactor: Post-review fixes

* ui: accept Windows roots in the working directory picker

recognize a drive root (C:) and a UNC share (//host/share) as path
navigation, alongside the POSIX root and ~, so a query like D:\repos
lists that directory instead of glob-matching it under the home dir

split below the root, so a bare drive resolves to its root rather than
to a drive-relative prefix

rewrite backslashes into forward slashes only when the query carries a
Windows root, since a backslash is a legal POSIX filename character

paths keep travelling with forward slashes, which is what the server
returns and what Windows accepts

---------

Co-authored-by: Pascal <admin@serveurperso.com>
2026-08-04 19:05:48 +02:00

94 lines
3.2 KiB
TypeScript

import { PATH_SEPARATOR } from '$lib/constants/mcp-resource';
import { TRAILING_SLASHES_REGEX } from '$lib/constants/url';
import {
CWD_CHANGED_PREFIX,
CWD_CLEARED_TEXT,
CWD_LINK_REGEX,
FILE_URI_PREFIX,
HOME_TILDE,
HOME_TILDE_PREFIX
} from '$lib/constants';
/**
* Last non-empty slash-delimited segment of `path`, with trailing
* slashes stripped. Returns the input unchanged when no `/` is present.
*/
export function lastPathSegment(p: string): string {
const trimmed = p.replace(TRAILING_SLASHES_REGEX, '');
const idx = trimmed.lastIndexOf(PATH_SEPARATOR);
return idx === -1 ? trimmed : trimmed.slice(idx + 1);
}
/**
* Abbreviate `path` to `~/...` when it sits under `home`, or to `~` when
* it equals `home`. Falls back to `lastPathSegment(path)` when home is
* unknown or the path is outside it. `~` semantics are reserved for the
* home directory, mirroring how shells render it.
*/
export function abbreviateWorkingDir(
path: string | null | undefined,
home: string | null | undefined
): string {
if (!path) return '';
if (!home) return lastPathSegment(path);
if (path === home) return HOME_TILDE;
if (path.startsWith(home + PATH_SEPARATOR))
return HOME_TILDE_PREFIX + path.slice(home.length + 1);
return lastPathSegment(path);
}
/**
* Replace a leading `home` prefix in `path` with `~`. Unlike
* abbreviateWorkingDir, paths outside `home` (or an unknown home) are
* returned unchanged - used for tool-call path displays where the full
* path matters.
*/
export function abbreviateHome(path: string, home: string | null | undefined): string {
if (!home) return path;
if (path === home) return HOME_TILDE;
if (path.startsWith(home + PATH_SEPARATOR))
return HOME_TILDE_PREFIX + path.slice(home.length + 1);
return path;
}
export { CWD_CHANGED_PREFIX, CWD_CLEARED_TEXT } from '$lib/constants';
export interface CwdMessageInfo {
// absolute server-side path, null when the cwd was cleared
path: string | null;
// display form shown in the UI (e.g. ~/Documents)
display: string;
}
/**
* Format a synthetic cwd-change message. The text mirrors what the UI
* renders for it; the path travels as `[file:///abs/path](display)` so
* both the absolute and the short form are visible to the model and
* parseable back by the UI.
*/
export function formatCwdMessage(cwd: string, home: string | null): string {
const display = abbreviateWorkingDir(cwd, home);
return `${CWD_CHANGED_PREFIX}[${FILE_URI_PREFIX}${cwd}](${display}).`;
}
/**
* Parse a synthetic cwd message back into its parts. The caller must already
* know the message is synthetic (via the persisted `isSynthetic` flag); this
* only extracts the path from the message text. Returns null when `content`
* is not a cwd message.
*/
export function parseCwdMessage(content: string): CwdMessageInfo | null {
const trimmed = content.trim();
if (trimmed === CWD_CLEARED_TEXT) {
return { path: null, display: '' };
}
if (trimmed.startsWith(CWD_CHANGED_PREFIX)) {
const rest = trimmed.slice(CWD_CHANGED_PREFIX.length);
// not anchored to the end: guidance may follow the link
const link = rest.match(CWD_LINK_REGEX);
if (link) return { path: link[1], display: link[2] };
return { path: rest, display: rest };
}
return null;
}