fix(ui): name the branch node in view answers and a folder by its path

The answer about the current view listed the branch node by its bare
"DETACHED", and the card of a folder said "Selected: .github (Folder) in
.github". Both now use the names the Galaxy shows.

Signed-off-by: Bernhard Jackiewicz <bj@techport.io>
This commit is contained in:
Bernhard Jackiewicz committed 2026-10-04 19:19:51 +02:00
1 parent ab4d61d63b
commit ff3e88b238
5 files changed
+34 -10

No files matched your search

@@ -16,7 +16,7 @@ describe('questions about the current view are answered from the loaded scope (H
await dock.ask(question);
const answer = dock.answerOf(dock.last()).replace(/\s+/g, ' ');
expect(answer).toContain('.github (Ordner) steht in der Mitte; der Ausschnitt geht 1 Schritt in beide Richtungen.');
expect(answer).toContain('Links, eingehend: CONTAINS_FOLDER (1): DETACHED');
expect(answer).toContain('Links, eingehend: CONTAINS_FOLDER (1): django-demo · losgelöster HEAD');
expect(answer).toContain('Rechts, ausgehend: CONTAINS_FILE (4): CODE_OF_CONDUCT.md, FUNDING.yml, pull_request_template.md, SECURITY.md CONTAINS_FOLDER (1): workflows');
expect(answer).toContain('So liest du die Hierarchie: eingehende Beziehungen stehen links, ausgehende rechts.');
expect(answer).toContain('Aus dem indizierten Graphen gelistet, nicht vom Modell erzeugt.');
@@ -57,7 +57,7 @@ describe('a model answer that only restates the question is not shown as an answ
expect(runtime.chat).toHaveBeenCalledOnce();
const answer = dock.answerOf(dock.last());
expect(answer).toContain('Das Modell hat keine Antwort gegeben, es hat nur die Frage wiederholt („Ich kann dir die Ordner erklären.“).');
expect(answer).toContain('Ausgewählt: .github (Ordner) in .github.');
expect(answer).toContain('Ausgewählt: .github (Ordner).');
expect(answer).toContain('Ausgehend: 5 Beziehungen zu 5 Symbolen (CONTAINS_FILE 4, CONTAINS_FOLDER 1).');
expect(answer).toContain('Aus dem indizierten Graphen gelistet, nicht vom Modell erzeugt.');
expect(dock.notesOf(dock.last())).toEqual([]);
@@ -0,0 +1,17 @@
import { describe, expect, it } from 'vitest';
import { readGalaxyEvidence, selectionSentence } from './galaxy-evidence';
import { githubFolderEvidence } from './galaxy-evidence.fixture';
import { relationshipWords } from './strings';
/*
* Handtest 04.10. (17:09): Die Karte zu .github sagte "Selected: .github (Folder)
* in .github." Bei einem Ordner oder einer Datei ist der Pfad das, was gewaehlt
* ist, kein Ort, an dem es steht; "in .github" wiederholte nur den Namen.
*/
describe('the selection sentence of a folder or a file', () => {
it('names a folder without repeating its own path as its place', () => {
const evidence = readGalaxyEvidence(githubFolderEvidence('hierarchy').text)!;
expect(selectionSentence(evidence, relationshipWords.en)[0]).toBe('Selected: .github (Folder).');
expect(selectionSentence(evidence, relationshipWords.de)[0]).toBe('Ausgewählt: .github (Ordner).');
});
});
+6 -2
View File
@@ -183,12 +183,16 @@ export function selectionName(evidence: GalaxyEvidence, words: RelationshipWords
/** What is selected, in the words of the prompt (English) or of a question (C5). */
export function selectionSentence(evidence: GalaxyEvidence, words: RelationshipWords): string[] {
const [first] = evidence.roots;
const range = (root: GalaxyEvidence['roots'][number]) => root.filePath
// A folder or a file is its path; "Selected: .github (Folder) in .github." only repeated the name (K47).
const isPath = (root: GalaxyEvidence['roots'][number]) => /^(?:folder|file|directory)$/i.test(root.kind ?? '')
&& Boolean(root.filePath) && (root.filePath === root.name || root.filePath!.endsWith(`/${root.name}`));
const shown = (root: GalaxyEvidence['roots'][number]) => isPath(root) ? root.filePath! : rootName(root, words);
const range = (root: GalaxyEvidence['roots'][number]) => root.filePath && !isPath(root)
? ` in ${root.filePath}${root.startLine ? `:${root.startLine}${root.endLine && root.endLine !== root.startLine ? `-${root.endLine}` : ''}` : ''}` : '';
// Kinds in the words of the answer: "(Klasse)" in a German one (W8).
const kind = (root: GalaxyEvidence['roots'][number]) => root.kind ? ` (${words.kindName(root.kind)})` : '';
if (evidence.rootCount <= 1 && first) {
return [words.selected(`${rootName(first, words)}${kind(first)}${range(first)}`),
return [words.selected(`${shown(first)}${kind(first)}${range(first)}`),
...first.documentation ? [words.documentation(first.documentation)] : []];
}
if (!first) return [words.notInScope(selectionName(evidence, words), words.kindName(evidence.selectionKind))];
+4 -3
View File
@@ -43,7 +43,8 @@ describe('the listed answer about the current view (H1)', () => {
const answer = viewAnswer(evidence('hierarchy'), 'de');
expect(answer.split('\n\n')).toEqual([
'`.github` (Ordner) steht in der Mitte; der Ausschnitt geht 1 Schritt in beide Richtungen.',
'Links, eingehend:\n- **CONTAINS_FOLDER (1):** `DETACHED`',
// The branch node is named as the Galaxy names it, not by its bare "DETACHED" (K47).
'Links, eingehend:\n- **CONTAINS_FOLDER (1):** `django-demo · losgelöster HEAD`',
'Rechts, ausgehend:\n- **CONTAINS_FILE (4):** `CODE_OF_CONDUCT.md`, `FUNDING.yml`, `pull_request_template.md`, `SECURITY.md`\n- **CONTAINS_FOLDER (1):** `workflows`',
'So liest du die Hierarchie: eingehende Beziehungen stehen links, ausgehende rechts.',
'7 Symbole und 6 Beziehungen, alle Beziehungstypen; vollständig geladen.',
@@ -55,7 +56,7 @@ describe('the listed answer about the current view (H1)', () => {
const answer = viewAnswer(evidence('galaxy'), 'en');
expect(answer.split('\n\n')).toEqual([
'`.github` (Folder) is in the middle; the scope reaches 1 hop in both directions.',
'Incoming:\n- **CONTAINS_FOLDER (1):** `DETACHED`',
'Incoming:\n- **CONTAINS_FOLDER (1):** `django-demo · detached HEAD`',
'Outgoing:\n- **CONTAINS_FILE (4):** `CODE_OF_CONDUCT.md`, `FUNDING.yml`, `pull_request_template.md`, `SECURITY.md`\n- **CONTAINS_FOLDER (1):** `workflows`',
'The galaxy view shows this scope as a cloud. "hierarchy" at the top right lays the same scope out in columns: incoming on the left, outgoing on the right.',
'7 symbols and 6 relationships, all relationship types; fully loaded.',
@@ -84,7 +85,7 @@ describe('the listed answer about the current view (H1)', () => {
it('says nothing about left, right or a cloud when the evidence does not say which view is shown', () => {
const answer = viewAnswer(evidence(), 'en');
expect(answer).toContain('Incoming:\n- **CONTAINS_FOLDER (1):** `DETACHED`');
expect(answer).toContain('Incoming:\n- **CONTAINS_FOLDER (1):** `django-demo · detached HEAD`');
expect(answer).not.toMatch(/left|right|cloud/);
});
});
+5 -3
View File
@@ -1,5 +1,6 @@
import { nodeDisplayName } from '../galaxy/node-names';
import type { RelationshipGroup } from '../galaxy/selection-evidence';
import { scopeParts, sideLoaded, type GalaxyEvidence } from './galaxy-evidence';
import { scopeParts, selectionName, sideLoaded, type GalaxyEvidence } from './galaxy-evidence';
import { relationshipWords, viewText, type RelationshipWords } from './strings';
/** The answer to a question about the current Galaxy view, listed from the loaded scope (H1).
@@ -16,7 +17,8 @@ const quote = (value: string) => `\`${value.replace(/`/g, "'")}\``;
/** "- **CONTAINS_FILE (4):** `CODE_OF_CONDUCT.md`, `FUNDING.yml`; +2 more". */
function typeLine(group: RelationshipGroup, words: RelationshipWords): string {
const shown = group.files.flatMap(file => file.symbols.map(symbol => symbol.name)).slice(0, NAMES_PER_TYPE);
// Named as the Galaxy names them: the branch node as "django-demo · detached HEAD", not "DETACHED" (K47).
const shown = group.files.flatMap(file => file.symbols.map(symbol => nodeDisplayName(symbol, words.nodeNames))).slice(0, NAMES_PER_TYPE);
const rest = group.count - shown.length;
const names = shown.length ? `${shown.map(quote).join(', ')}${rest > 0 ? `; ${words.more(rest)}` : ''}` : words.more(group.count);
return `- **${words.typeCount(group.type, group.count)}:** ${names}`;
@@ -28,7 +30,7 @@ export function viewAnswer(evidence: GalaxyEvidence, language: 'en' | 'de'): str
const hierarchy = evidence.display === 'hierarchy';
const [first] = evidence.roots;
const single = evidence.rootCount <= 1 && first !== undefined;
const center = text.center(quote(single ? first.name : evidence.label), words.kindName((single ? first.kind : undefined) ?? evidence.selectionKind), single ? 1 : evidence.rootCount);
const center = text.center(quote(single ? nodeDisplayName(first, words.nodeNames) : selectionName(evidence, words)), words.kindName((single ? first.kind : undefined) ?? evidence.selectionKind), single ? 1 : evidence.rootCount);
const { direction, types, state } = scopeParts(evidence, words);
const reach = evidence.depth === 0 ? text.selectionOnly : text.reach(words.hops(evidence.depth), direction, evidence.direction === 'both');
const side = (name: 'incoming' | 'outgoing'): string => {