@timurproko/a1 0.2.7-dev.1268 → 0.2.7-dev.1269

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 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. Disable **Automatic session name** in Agent settings; see [session names](docs/features/session-naming.md) for details.
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
- /** Booleans read as yes and no. */
824
- function displaySettingValue(_entry, value) {
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 generateSessionName(prompt, `${context.cwd.split(/[\\/]/).at(-1) ?? ""} (${context.branch ?? "no branch"})`, request => this.#options.naming.complete(request), work.signal).then(name => {
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
- return this.#options.naming.apply(state, name, key);
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 generateSessionName(prompt: string, hints: string, complete: (request: OwnedUiSessionNameCompletion) => Promise<string | null>, signal: AbortSignal, timeoutMs?: number): Promise<string>;
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 normalize(value) {
2
- const name = value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
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(/-+$/g, "") || null;
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 generateSessionName(prompt, hints, complete, signal, timeoutMs = 30_000) {
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 a lowercase hyphenated title 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>`,
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
- const name = response === null ? null : normalize(response);
30
- if (name !== null)
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
- return normalize((prompt.toLowerCase().match(/[a-z0-9]+/g) ?? []).slice(0, 3).join("-")) ?? "new-task";
52
+ const fallback = (prompt.match(/[a-z0-9]+/gi) ?? []).slice(0, 3).join(" ");
53
+ return fallback || "new task";
44
54
  }
@@ -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 lowercase hyphenated labels (up to 16 characters), 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.
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.1268",
3
+ "version": "0.2.7-dev.1269",
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": "5611d6339b9a320baebd3329b3abd85a810c48f9",
117
- "pullRequest": 895
116
+ "source": "0783454e6931f7478be0a1bbfe27d9e886a0c3a3",
117
+ "pullRequest": 896
118
118
  }
119
119
  }