Skip to content

Commit 778a630

Browse files
fix(workspace): open the same-name question on No, and compare names once per list
The TUI's duplicate-name question used a confirm dialog that opens on Confirm, so a stray Enter created the duplicate; it is now a select that opens on No. The namesake is computed once per list, the CLI prompt keeps a server-supplied name on one line, and naming and comparing share one normaliser. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0172qrhMa5TQgETASi5hxMqD
1 parent 0ebb311 commit 778a630

3 files changed

Lines changed: 34 additions & 19 deletions

File tree

‎packages/opencode/src/altimate/workspace/workspace-name.ts‎

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,7 @@
1515
* what the model reads. */
1616
export const MAX_WORKSPACE_NAME_CHARS = 80
1717
export function inertWorkspaceName(name: string): string {
18-
const cleaned = name
19-
.replace(/[\u0000-\u001F\u007F-\u009F\u2028\u2029]+/g, " ")
20-
.replace(/\s+/g, " ")
21-
.trim()
18+
const cleaned = oneLine(name)
2219
const points = Array.from(cleaned)
2320
return points.length > MAX_WORKSPACE_NAME_CHARS ? points.slice(0, MAX_WORKSPACE_NAME_CHARS - 1).join("") + "…" : cleaned
2421
}
@@ -48,15 +45,20 @@ export function workspaceLabel(name: string, id: string | undefined, budget = MA
4845
return label.length > budget ? suffix.trim() : label
4946
}
5047

51-
/** A name as the link pickers compare it: control characters and runs of whitespace
52-
* collapsed, trimmed, and the sharp S spelled out, which collation otherwise keeps
53-
* apart from "ss". */
54-
function comparableName(name: string): string {
48+
/** A name on one line: control characters (C0, DEL and C1) and the Unicode line and
49+
* paragraph separators become spaces, runs of whitespace collapse, ends are trimmed.
50+
* The one normalisation both the rendered and the compared name start from. */
51+
function oneLine(name: string): string {
5552
return name
5653
.replace(/[\u0000-\u001F\u007F-\u009F\u2028\u2029]+/g, " ")
5754
.replace(/\s+/g, " ")
5855
.trim()
59-
.replace(/[ßẞ]/g, "ss")
56+
}
57+
58+
/** A name as the link pickers compare it: on one line, and the sharp S spelled out,
59+
* which collation otherwise keeps apart from "ss". */
60+
function comparableName(name: string): string {
61+
return oneLine(name).replace(/[ßẞ]/g, "ss")
6062
}
6163

6264
/** The first listed workspace already named what a quick create would call this

‎packages/opencode/src/cli/cmd/link.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ import {
4343
} from "@/altimate/workspace/browser-handoff"
4444
import { accountDigest, recordApprovedBinding } from "@/altimate/workspace/state"
4545
import type { SeedOutcome } from "@/altimate/workspace/memory-backfill"
46-
import { sameNamedWorkspace } from "@/altimate/workspace/workspace-name"
46+
import { inertWorkspaceName, sameNamedWorkspace } from "@/altimate/workspace/workspace-name"
4747

4848
const CREATE_NEW_SENTINEL = "__create_new__"
4949
const SET_UP_IN_BROWSER_SENTINEL = "__browser_handoff__"
@@ -342,7 +342,7 @@ export const LinkCommand = cmd({
342342
// Both create paths start from the project's name, so both confirm a namesake.
343343
if ((pick === SET_UP_IN_BROWSER_SENTINEL || pick === CREATE_NEW_SENTINEL) && namesake) {
344344
const again = await prompts.confirm({
345-
message: `A workspace named "${stripControlChars(namesake.name)}" already exists. Create another one with the same name?`,
345+
message: `A workspace named "${inertWorkspaceName(namesake.name)}" already exists. Create another one with the same name?`,
346346
initialValue: false,
347347
})
348348
if (prompts.isCancel(again) || !again) {

‎packages/opencode/src/plugin/tui/altimate/workspace.tsx‎

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ import * as Manage from "@/altimate/workspace/manage"
3131
import { describeSyncProblems } from "@/altimate/workspace/skill-sync"
3232
import { inertWorkspaceName, sameNamedWorkspace } from "@/altimate/workspace/workspace-name"
3333
// altimate_change end
34-
import { createSignal, onCleanup, onMount } from "solid-js"
34+
import { createMemo, createSignal, onCleanup, onMount } from "solid-js"
3535
import {
3636
ConflictError,
3737
HIDDEN_BINDING_MESSAGE,
@@ -1000,10 +1000,11 @@ function OnDemandPickerDialog(props: OnDemandPickerProps) {
10001000

10011001
// A listed workspace already named what a quick create would use: the picker
10021002
// opens on it, and creating a namesake is confirmed first.
1003-
const namesake = () => {
1003+
// Once per list, not once per row: the options below read it for every workspace.
1004+
const namesake = createMemo(() => {
10041005
const list = datamates()
10051006
return list ? sameNamedWorkspace(list, props.defaultName) : undefined
1006-
}
1007+
})
10071008

10081009
const options = () => {
10091010
const list = datamates()
@@ -1059,12 +1060,24 @@ function OnDemandPickerDialog(props: OnDemandPickerProps) {
10591060
create()
10601061
return
10611062
}
1063+
// A select rather than DialogConfirm, which opens on Confirm: here a
1064+
// stray Enter must not create the duplicate, so the dialog opens on No.
10621065
props.api.ui.dialog.replace(() => (
1063-
<props.api.ui.DialogConfirm
1064-
title="Create a workspace with the same name?"
1065-
message={`A workspace named "${inertWorkspaceName(twin.name)}" already exists. Create another one called "${props.defaultName}"?`}
1066-
onConfirm={create}
1067-
onCancel={() => props.api.ui.dialog.clear()}
1066+
<props.api.ui.DialogSelect<string>
1067+
title={`A workspace named "${inertWorkspaceName(twin.name)}" already exists`}
1068+
options={[
1069+
{ title: "No, don't create it", value: "no", description: "Nothing changes." },
1070+
{
1071+
title: `Yes, create another "${props.defaultName}"`,
1072+
value: "yes",
1073+
description: "Two workspaces will share this name.",
1074+
},
1075+
]}
1076+
current="no"
1077+
onSelect={(choice) => {
1078+
if (choice.value === "yes") create()
1079+
else props.api.ui.dialog.clear()
1080+
}}
10681081
/>
10691082
))
10701083
return

0 commit comments

Comments
 (0)