mirror of
https://github.com/ggml-org/llama.cpp.git
synced 2026-09-08 13:59:03 +02:00
2f56fc3431
* 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>
127 lines
3.7 KiB
TypeScript
127 lines
3.7 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
import {
|
|
splitPathQuery,
|
|
buildCaseInsensitiveGlob,
|
|
rankEntries,
|
|
joinPath,
|
|
highlightMatch
|
|
} from '$lib/utils';
|
|
|
|
describe('splitPathQuery', () => {
|
|
it('treats a plain query as a home-relative glob (not navigation)', () => {
|
|
expect(splitPathQuery('docs')).toBeNull();
|
|
});
|
|
|
|
it('navigates the root for `/`', () => {
|
|
expect(splitPathQuery('/')).toEqual({ parent: '/', last: '' });
|
|
});
|
|
|
|
it('navigates home for `~`', () => {
|
|
expect(splitPathQuery('~')).toEqual({ parent: '~', last: '' });
|
|
});
|
|
|
|
it('splits an absolute path into parent and last segment', () => {
|
|
expect(splitPathQuery('/Users/al/proj')).toEqual({ parent: '/Users/al', last: 'proj' });
|
|
});
|
|
|
|
it('navigates a Windows drive path written with backslashes', () => {
|
|
expect(splitPathQuery('C:\\repos\\llama.cpp')).toEqual({
|
|
parent: 'C:/repos',
|
|
last: 'llama.cpp'
|
|
});
|
|
});
|
|
|
|
it('navigates a Windows drive path written with forward slashes', () => {
|
|
expect(splitPathQuery('D:/repos')).toEqual({ parent: 'D:/', last: 'repos' });
|
|
});
|
|
|
|
it('treats a bare drive as its root', () => {
|
|
expect(splitPathQuery('D:')).toEqual({ parent: 'D:/', last: '' });
|
|
expect(splitPathQuery('D:\\')).toEqual({ parent: 'D:/', last: '' });
|
|
});
|
|
|
|
it('navigates a UNC share', () => {
|
|
expect(splitPathQuery('\\\\host\\share\\proj')).toEqual({
|
|
parent: '//host/share/',
|
|
last: 'proj'
|
|
});
|
|
});
|
|
|
|
it('keeps a backslash as a POSIX filename character', () => {
|
|
expect(splitPathQuery('/tmp/a\\b')).toEqual({ parent: '/tmp', last: 'a\\b' });
|
|
});
|
|
|
|
it('splits a home-relative path into parent and last segment', () => {
|
|
expect(splitPathQuery('~/Documents')).toEqual({ parent: '~', last: 'Documents' });
|
|
});
|
|
|
|
it('strips trailing slashes before splitting', () => {
|
|
expect(splitPathQuery('/Users/al/')).toEqual({ parent: '/Users', last: 'al' });
|
|
});
|
|
|
|
it('handles a single-segment absolute path', () => {
|
|
expect(splitPathQuery('/opt')).toEqual({ parent: '/', last: 'opt' });
|
|
});
|
|
});
|
|
|
|
describe('buildCaseInsensitiveGlob', () => {
|
|
it('wraps letters in case-insensitive character classes', () => {
|
|
expect(buildCaseInsensitiveGlob('ab')).toBe('*[aA][bB]*');
|
|
});
|
|
|
|
it('escapes glob metacharacters into literal fragments', () => {
|
|
expect(buildCaseInsensitiveGlob('a*b')).toBe('*[aA][*][bB]*');
|
|
});
|
|
});
|
|
|
|
describe('rankEntries', () => {
|
|
const entries = [
|
|
{ path: '/h/README', type: 'dir' },
|
|
{ path: '/h/read', type: 'dir' },
|
|
{ path: '/h/readme.txt', type: 'dir' }
|
|
];
|
|
|
|
it('ranks exact basename match first', () => {
|
|
const ranked = rankEntries(entries, 'read');
|
|
expect(ranked[0].path).toBe('/h/read');
|
|
});
|
|
|
|
it('breaks ties by shorter path, then alphabetically', () => {
|
|
const ranked = rankEntries(entries, 'read');
|
|
expect(ranked[ranked.length - 1].path).toBe('/h/readme.txt');
|
|
});
|
|
|
|
it('does not mutate the input', () => {
|
|
const snapshot = [...entries];
|
|
rankEntries(entries, 'read');
|
|
expect(entries).toEqual(snapshot);
|
|
});
|
|
});
|
|
|
|
describe('joinPath', () => {
|
|
it('joins base and relative avoiding a double slash', () => {
|
|
expect(joinPath('/home/al/', 'docs')).toBe('/home/al/docs');
|
|
});
|
|
|
|
it('returns the relative path when base is empty', () => {
|
|
expect(joinPath('', 'docs')).toBe('docs');
|
|
});
|
|
});
|
|
|
|
describe('highlightMatch', () => {
|
|
it('returns a single non-matching segment when query is empty', () => {
|
|
expect(highlightMatch('abc', '')).toEqual([{ text: 'abc', match: false }]);
|
|
});
|
|
|
|
it('marks every case-insensitive occurrence of the query', () => {
|
|
expect(highlightMatch('aXa', 'ax')).toEqual([
|
|
{ text: 'aX', match: true },
|
|
{ text: 'a', match: false }
|
|
]);
|
|
});
|
|
|
|
it('returns non-matching text when the query is absent', () => {
|
|
expect(highlightMatch('abc', 'z')).toEqual([{ text: 'abc', match: false }]);
|
|
});
|
|
});
|