@timurproko/a1 0.2.7-dev.1268 → 0.2.7-dev.1270
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/README.md +1 -1
- package/dist/composition/session-naming.js +1 -0
- package/dist/features/owned-ui/settings-app.js +10 -2
- package/dist/features/session-naming/controller.d.ts +2 -0
- package/dist/features/session-naming/controller.js +4 -3
- package/dist/features/session-naming/generate.d.ts +3 -1
- package/dist/features/session-naming/generate.js +20 -10
- package/dist/ui/components/scrollbar.d.ts +2 -2
- package/dist/ui/components/scrollbar.js +2 -2
- package/dist/ui/components/surface.js +1 -1
- package/dist/ui/settings/declarations.d.ts +12 -0
- package/dist/ui/settings/declarations.js +9 -0
- package/docs/features/session-naming.md +15 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -67,7 +67,7 @@ a1 --session <id>
|
|
|
67
67
|
|
|
68
68
|
When a session exits, A1 prints the `a1 --session <id>` command that resumes it; a custom session store is included in the printed command. Closing the terminal ends the agent, as it does for Pi.
|
|
69
69
|
|
|
70
|
-
Bare A1 automatically names unnamed sessions from the first prompt and regenerates automatic names on the next prompt after a worktree-context change. Manual `/name` renames are preserved. Names appear after footer stats, with a secondary-accent reveal for automatic changes.
|
|
70
|
+
Bare A1 automatically names unnamed sessions from the first prompt and regenerates automatic names on the next prompt after a worktree-context change. Manual `/name` renames are preserved. Names appear after footer stats, with a secondary-accent reveal for automatic changes. Agent settings offers **Session name format** with Kebab case (default), Sentence case, and Title Case for future generated names, or **Automatic session name** to disable naming; see [session names](docs/features/session-naming.md) for details.
|
|
71
71
|
|
|
72
72
|
Show all commands:
|
|
73
73
|
|
|
@@ -6,6 +6,7 @@ export function bindAutomaticSessionNaming(backend, settings, bare) {
|
|
|
6
6
|
const naming = new SessionNamingController({
|
|
7
7
|
backend, naming: backend.naming,
|
|
8
8
|
enabled: () => settings?.value("sessionAutoName") ?? true,
|
|
9
|
+
format: () => settings?.value("sessionNameFormat") ?? "kebab",
|
|
9
10
|
withContext: async (consume, signal) => {
|
|
10
11
|
const sessionId = backend.identity.currentHistorySessionId();
|
|
11
12
|
const sessionFile = backend.identity.currentSessionFile();
|
|
@@ -820,10 +820,18 @@ function labelOf(entry) {
|
|
|
820
820
|
function rangeOf(entry) {
|
|
821
821
|
return { minimum: entry.minimum, maximum: entry.maximum, values: numericValues(entry.choices) };
|
|
822
822
|
}
|
|
823
|
-
/**
|
|
824
|
-
function displaySettingValue(
|
|
823
|
+
/** Scalar choices use their readable presentation without changing stored values. */
|
|
824
|
+
function displaySettingValue(entry, value) {
|
|
825
825
|
if (typeof value === "boolean")
|
|
826
826
|
return value ? "yes" : "no";
|
|
827
|
+
if (entry.backend === "a1" && entry.id === "sessionNameFormat") {
|
|
828
|
+
if (value === "kebab")
|
|
829
|
+
return "Kebab case";
|
|
830
|
+
if (value === "sentence")
|
|
831
|
+
return "Sentence case";
|
|
832
|
+
if (value === "title")
|
|
833
|
+
return "Title Case";
|
|
834
|
+
}
|
|
827
835
|
return String(value);
|
|
828
836
|
}
|
|
829
837
|
function describeRaw(value) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { OwnedUiSessionBackend, OwnedUiSessionNamingPort } from "../../contracts/owned-ui/index.js";
|
|
2
|
+
import type { SessionNameFormat } from "./generate.js";
|
|
2
3
|
export interface SessionNamingContext {
|
|
3
4
|
readonly cwd: string;
|
|
4
5
|
readonly branch: string | null;
|
|
@@ -10,6 +11,7 @@ interface NamingOptions {
|
|
|
10
11
|
/** Executes the synchronous callback under authoritative repository-context coordination. */
|
|
11
12
|
readonly withContext: <T>(consume: (context: SessionNamingContext) => T, signal: AbortSignal) => Promise<T>;
|
|
12
13
|
readonly enabled: () => boolean;
|
|
14
|
+
readonly format: () => SessionNameFormat;
|
|
13
15
|
}
|
|
14
16
|
/** One background naming chain per eligible prompt/context; owns no timers or engine objects. */
|
|
15
17
|
export declare class SessionNamingController {
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { generateSessionName } from "./generate.js";
|
|
2
1
|
function contextKey(context) {
|
|
3
2
|
return JSON.stringify([context.cwd.replace(/\\/g, "/"), context.branch, context.revision]);
|
|
4
3
|
}
|
|
@@ -134,13 +133,15 @@ export class SessionNamingController {
|
|
|
134
133
|
this.#attempted = key;
|
|
135
134
|
const work = new AbortController();
|
|
136
135
|
this.#work = { abort: work, key: repositoryKey };
|
|
137
|
-
void
|
|
136
|
+
void import("./generate.js").then(async ({ formatSessionName, generateSessionNameCandidate }) => {
|
|
137
|
+
const candidate = await generateSessionNameCandidate(prompt, `${context.cwd.split(/[\\/]/).at(-1) ?? ""} (${context.branch ?? "no branch"})`, request => this.#options.naming.complete(request), work.signal, this.#options.format);
|
|
138
138
|
if (work.signal.aborted || this.#disposed || !this.#options.enabled())
|
|
139
139
|
return;
|
|
140
140
|
return this.#options.withContext(current => {
|
|
141
141
|
if (contextKey(current) !== repositoryKey || work.signal.aborted || this.#disposed || !this.#options.enabled())
|
|
142
142
|
return false;
|
|
143
|
-
|
|
143
|
+
const name = formatSessionName(candidate, this.#options.format());
|
|
144
|
+
return name !== null && this.#options.naming.apply(state, name, key);
|
|
144
145
|
}, work.signal);
|
|
145
146
|
}).catch(() => { }).finally(() => { if (this.#work?.abort === work)
|
|
146
147
|
this.#work = undefined; });
|
|
@@ -1,3 +1,5 @@
|
|
|
1
1
|
import type { OwnedUiSessionNameCompletion } from "../../contracts/owned-ui/index.js";
|
|
2
|
+
export type SessionNameFormat = "kebab" | "sentence" | "title";
|
|
3
|
+
export declare function formatSessionName(value: string, format: SessionNameFormat): string | null;
|
|
2
4
|
/** Two bounded tool-free attempts; cancellation never becomes an automatic fallback. */
|
|
3
|
-
export declare function
|
|
5
|
+
export declare function generateSessionNameCandidate(prompt: string, hints: string, complete: (request: OwnedUiSessionNameCompletion) => Promise<string | null>, signal: AbortSignal, format: () => SessionNameFormat, timeoutMs?: number): Promise<string>;
|
|
@@ -1,14 +1,22 @@
|
|
|
1
|
-
function
|
|
2
|
-
const
|
|
1
|
+
export function formatSessionName(value, format) {
|
|
2
|
+
const words = (format === "kebab" ? value.toLowerCase() : value).match(/[a-z0-9]+/gi) ?? [];
|
|
3
|
+
const name = words.map((word, index) => {
|
|
4
|
+
if (format === "kebab")
|
|
5
|
+
return word;
|
|
6
|
+
if (/^[A-Z0-9]+$/.test(word) || /[a-z][A-Z]/.test(word))
|
|
7
|
+
return word;
|
|
8
|
+
const lower = word.toLowerCase();
|
|
9
|
+
return format === "title" || index === 0 ? lower[0].toUpperCase() + lower.slice(1) : lower;
|
|
10
|
+
}).join(format === "kebab" ? "-" : " ");
|
|
3
11
|
if (!name)
|
|
4
12
|
return null;
|
|
5
13
|
if (name.length <= 16)
|
|
6
14
|
return name;
|
|
7
|
-
const boundary = name.slice(0, 17).lastIndexOf("-");
|
|
8
|
-
return (boundary >= 4 ? name.slice(0, boundary) : name.slice(0, 16)).replace(
|
|
15
|
+
const boundary = name.slice(0, 17).lastIndexOf(format === "kebab" ? "-" : " ");
|
|
16
|
+
return (boundary >= 4 ? name.slice(0, boundary) : name.slice(0, 16)).replace(/[- ]+$/g, "") || null;
|
|
9
17
|
}
|
|
10
18
|
/** Two bounded tool-free attempts; cancellation never becomes an automatic fallback. */
|
|
11
|
-
export async function
|
|
19
|
+
export async function generateSessionNameCandidate(prompt, hints, complete, signal, format, timeoutMs = 30_000) {
|
|
12
20
|
for (const maxTokens of [4_096, 32_768]) {
|
|
13
21
|
signal.throwIfAborted();
|
|
14
22
|
const deadline = new AbortController();
|
|
@@ -21,14 +29,15 @@ export async function generateSessionName(prompt, hints, complete, signal, timeo
|
|
|
21
29
|
aborted = () => reject(attempt.reason);
|
|
22
30
|
attempt.addEventListener("abort", aborted, { once: true });
|
|
23
31
|
});
|
|
32
|
+
const requestedFormat = format();
|
|
33
|
+
const style = requestedFormat === "kebab" ? "a lowercase hyphenated title" : `a ${requestedFormat === "sentence" ? "sentence case" : "title case"} title with spaces, preserving acronyms and technical spelling`;
|
|
24
34
|
const response = await Promise.race([complete({
|
|
25
|
-
prompt: `Name this coding task with
|
|
35
|
+
prompt: `Name this coding task with ${style} of at most 16 characters. Reply only with the title.\nRepository hints: ${hints.slice(0, 300)}\n<task>\n${prompt.slice(0, 500)}\n</task>`,
|
|
26
36
|
maxTokens, signal: attempt,
|
|
27
37
|
}), cancelled]);
|
|
28
38
|
signal.throwIfAborted();
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
return name;
|
|
39
|
+
if (response !== null && formatSessionName(response, format()) !== null)
|
|
40
|
+
return response;
|
|
32
41
|
}
|
|
33
42
|
catch {
|
|
34
43
|
signal.throwIfAborted();
|
|
@@ -40,5 +49,6 @@ export async function generateSessionName(prompt, hints, complete, signal, timeo
|
|
|
40
49
|
}
|
|
41
50
|
}
|
|
42
51
|
signal.throwIfAborted();
|
|
43
|
-
|
|
52
|
+
const fallback = (prompt.match(/[a-z0-9]+/gi) ?? []).slice(0, 3).join(" ");
|
|
53
|
+
return fallback || "new task";
|
|
44
54
|
}
|
|
@@ -38,8 +38,8 @@ export interface ScrollbarPresentationInput {
|
|
|
38
38
|
export interface ScrollbarPresentation {
|
|
39
39
|
readonly visible: boolean;
|
|
40
40
|
readonly reservesSpace: boolean;
|
|
41
|
-
readonly trackGlyph: "
|
|
42
|
-
readonly thumbGlyph: "
|
|
41
|
+
readonly trackGlyph: "┃" | "▐";
|
|
42
|
+
readonly thumbGlyph: "┃" | "▐";
|
|
43
43
|
}
|
|
44
44
|
/** Pure appearance decision; activity time is supplied so tests own the clock. */
|
|
45
45
|
export declare function scrollbarPresentation(input: ScrollbarPresentationInput): ScrollbarPresentation;
|
|
@@ -26,8 +26,8 @@ export function scrollbarPresentation(input) {
|
|
|
26
26
|
return {
|
|
27
27
|
visible,
|
|
28
28
|
reservesSpace,
|
|
29
|
-
trackGlyph: thick ? "
|
|
30
|
-
thumbGlyph: thick
|
|
29
|
+
trackGlyph: thick ? "▐" : "┃",
|
|
30
|
+
thumbGlyph: thick ? "▐" : "┃",
|
|
31
31
|
};
|
|
32
32
|
}
|
|
33
33
|
/** Null when the content fits: no scrollbar is drawn and no width is reserved. */
|
|
@@ -10,7 +10,7 @@ export const RAIL_COLUMNS = 2;
|
|
|
10
10
|
export function withScrollbarRail(lines, geometry, contentWidth, theme, options = {}) {
|
|
11
11
|
const inset = options.topInset ?? 0;
|
|
12
12
|
const presentation = options.presentation
|
|
13
|
-
?? { visible: geometry !== null, reservesSpace: true, trackGlyph: "
|
|
13
|
+
?? { visible: geometry !== null, reservesSpace: true, trackGlyph: "┃", thumbGlyph: "┃" };
|
|
14
14
|
if (!presentation.reservesSpace)
|
|
15
15
|
return lines;
|
|
16
16
|
const drawn = presentation.visible && geometry !== null;
|
|
@@ -154,6 +154,18 @@ export declare const OWNED_SETTING_DECLARATIONS: Readonly<{
|
|
|
154
154
|
defaultValue: true;
|
|
155
155
|
allowedValues: readonly [true, false];
|
|
156
156
|
}>;
|
|
157
|
+
readonly sessionNameFormat: Readonly<{
|
|
158
|
+
id: "sessionNameFormat";
|
|
159
|
+
label: "Session name format";
|
|
160
|
+
section: Readonly<{
|
|
161
|
+
id: "agent";
|
|
162
|
+
title: "Agent";
|
|
163
|
+
}>;
|
|
164
|
+
description: "Format for future automatic names: fix-api-cache, Fix API cache, or Fix API Cache. Existing and manual names stay unchanged.";
|
|
165
|
+
application: "live";
|
|
166
|
+
defaultValue: "kebab";
|
|
167
|
+
allowedValues: readonly ["kebab", "sentence", "title"];
|
|
168
|
+
}>;
|
|
157
169
|
readonly promptSuggestions: Readonly<{
|
|
158
170
|
id: "promptSuggestions";
|
|
159
171
|
label: "Prompt suggestions";
|
|
@@ -109,6 +109,15 @@ export const OWNED_SETTING_DECLARATIONS = Object.freeze({
|
|
|
109
109
|
defaultValue: true,
|
|
110
110
|
allowedValues: Object.freeze([true, false]),
|
|
111
111
|
}),
|
|
112
|
+
sessionNameFormat: Object.freeze({
|
|
113
|
+
id: "sessionNameFormat",
|
|
114
|
+
label: "Session name format",
|
|
115
|
+
section: AGENT_SECTION,
|
|
116
|
+
description: "Format for future automatic names: fix-api-cache, Fix API cache, or Fix API Cache. Existing and manual names stay unchanged.",
|
|
117
|
+
application: "live",
|
|
118
|
+
defaultValue: "kebab",
|
|
119
|
+
allowedValues: Object.freeze(["kebab", "sentence", "title"]),
|
|
120
|
+
}),
|
|
112
121
|
promptSuggestions: Object.freeze({
|
|
113
122
|
id: "promptSuggestions",
|
|
114
123
|
label: "Prompt suggestions",
|
|
@@ -1,9 +1,23 @@
|
|
|
1
1
|
# Session names
|
|
2
2
|
|
|
3
|
-
Bare A1 automatically names an unnamed session from its first prompt while the agent works. Names are short
|
|
3
|
+
Bare A1 automatically names an unnamed session from its first prompt while the agent works. Names are short labels (up to 16 characters, including separators), generated by the active model in a separate tool-free request. The request does not add a conversation turn or delay the main response. It makes at most two attempts, with 30-second deadlines and respective 4,096/32,768 token ceilings; provider failures use a deterministic prompt-based fallback.
|
|
4
4
|
|
|
5
5
|
**Automatic session name** in the Agent settings group is enabled by default. Turning it off cancels pending naming without deleting the current name. The `a1 pi` comparison profile does not receive automatic naming or the A1 footer layout/reveal.
|
|
6
6
|
|
|
7
|
+
## Name format
|
|
8
|
+
|
|
9
|
+
Choose **Session name format** in Agent settings:
|
|
10
|
+
|
|
11
|
+
| Choice | Example |
|
|
12
|
+
| --- | --- |
|
|
13
|
+
| **Kebab case** (default) | `fix-api-cache` |
|
|
14
|
+
| **Sentence case** | `Fix API cache` |
|
|
15
|
+
| **Title Case** | `Fix API Cache` |
|
|
16
|
+
|
|
17
|
+
Sentence case capitalizes the first word; Title Case capitalizes every ordinary word. Both preserve acronyms and technical spellings supplied by the model, such as `API` and `GitHub`. The same format applies to prompt-based fallback, and all formats keep the 16-character limit, trimming at a word boundary where possible.
|
|
18
|
+
|
|
19
|
+
The choice is saved to your A1 profile and applies live to future automatic naming, including context regeneration and pending results. Changing it does not rewrite an existing name or start another naming request. Manual `/name` values remain untouched.
|
|
20
|
+
|
|
7
21
|
## Task-context changes
|
|
8
22
|
|
|
9
23
|
Linking a different worktree, clearing and re-entering the same worktree, or changing its confirmed branch makes an automatic name eligible for regeneration. A durable association revision detects exit/re-entry even between footer refreshes. Repeating a link without leaving, runtime heartbeats, commits, dirty files, and PR-check updates do not trigger regeneration.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@timurproko/a1",
|
|
3
|
-
"version": "0.2.7-dev.
|
|
3
|
+
"version": "0.2.7-dev.1270",
|
|
4
4
|
"description": "Standalone single-agent terminal UI on pinned Pi",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"privateLaunchContract": "neutral-launch-v1",
|
|
@@ -113,7 +113,7 @@
|
|
|
113
113
|
"@mariozechner/clipboard": "^0.3.9"
|
|
114
114
|
},
|
|
115
115
|
"publication": {
|
|
116
|
-
"source": "
|
|
117
|
-
"pullRequest":
|
|
116
|
+
"source": "8c5a4d03abf3e403a929e81078823d286a2f860f",
|
|
117
|
+
"pullRequest": 897
|
|
118
118
|
}
|
|
119
119
|
}
|