Skip to content

Commit 53ad999

Browse files
authored
fix: small workspace polish: hints for dead shortcuts, archived in All Projects, bold and strike while typing (#683)
- The rail shows no account avatar until the host names its user, instead of a placeholder `A`. - ⌘3/⌘4 in a project without GitHub, and Cmd+O or Clone while disconnected, say why they did nothing. - All Projects lists your archived channels in an Archived group, as the archived notice promises. - `**bold**` and `~~strike~~` convert as their closing marker is typed. - The .gitignore card gives the git commands to copy. - Team pulse entries show when the run ended and cover at least the last day, so a reload no longer clears them.
1 parent 753b28e commit 53ad999

9 files changed

Lines changed: 104 additions & 31 deletions

File tree

‎.lane/memory/apps/app/src/app.tsx/01M4YT6XB7NQ89E6F4P375REE7-all-projects-and-project-vie.md‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@ anchor: function App
44
created: 2026-10-15T03:38:04Z
55
norm: '1'
66
sig: 063d0f00fea02511
7-
body_hash: 85707ad58ff21113
8-
raw_hash: 847e872f7c9185bd
9-
vouched: 2026-10-22T22:40:41Z
7+
body_hash: cffc6c2ce7bd7e6d
8+
raw_hash: 14cee7a4562529f6
9+
vouched: 2026-10-23T12:32:53Z
1010
lines: 235-824
1111
---
1212

‎.lane/memory/apps/app/src/app.tsx/01M5JMVG9G87F31FGVV571Y6BJ-the-project-list-is-built-on.md‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@ anchor: function App
44
created: 2026-10-22T20:29:16Z
55
norm: '1'
66
sig: 063d0f00fea02511
7-
body_hash: 85707ad58ff21113
8-
raw_hash: 847e872f7c9185bd
9-
vouched: 2026-10-22T22:40:41Z
7+
body_hash: cffc6c2ce7bd7e6d
8+
raw_hash: 14cee7a4562529f6
9+
vouched: 2026-10-23T12:32:53Z
1010
lines: 330-1237
1111
---
1212

‎.lane/memory/apps/app/src/open-project.tsx/01M5JNQ72H3WS44JG82D60Q37M-a-folder-without-a-commit-is.md‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@ anchor: function InitGit
44
created: 2026-10-22T20:44:24Z
55
norm: '1'
66
sig: bbe6f480fcc6cbd1
7-
body_hash: 0fc448d2189f968d
8-
raw_hash: 517902d11c496496
9-
vouched: 2026-10-22T22:08:11Z
7+
body_hash: bc4e9763ce66dd48
8+
raw_hash: 9a78d51c3d8b3e6f
9+
vouched: 2026-10-23T12:32:53Z
1010
lines: 52-111
1111
---
1212

‎.lane/memory/packages/ui/src/components/rich-text/commands.ts/01M4YQHWK1MX9HXNT9001NFSC1-markdown-markers-become-form.md‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@ anchor: function markdownRules
44
created: 2026-10-15T02:51:38Z
55
norm: '1'
66
sig: 0a8b81082cdfba48
7-
body_hash: 5c98630ded958a04
8-
raw_hash: 21ce709a783c8571
7+
body_hash: cdce281d3c2d2b7c
8+
raw_hash: 30247d4f6ad4c50d
9+
vouched: 2026-10-23T12:32:53Z
910
lines: 221-240
1011
---
1112

‎apps/app/src/app.tsx‎

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -479,7 +479,15 @@ export function App() {
479479
const linked = !!repo.value || (status === "open" && !!repo.error);
480480
const page = repo.value === null && (saved === "issues" || saved === "prs") ? "channels" : saved;
481481
function go(next: Page) {
482-
if (linked || (next !== "issues" && next !== "prs")) setPage(next);
482+
if (linked || (next !== "issues" && next !== "prs")) return setPage(next);
483+
// ⌘3, ⌘4, and the menu reach Issues and PRs even where the rail hides them.
484+
const what = next === "issues" ? "Issues" : "PRs";
485+
if (!current) return void toast(`${what} need a project on GitHub`);
486+
if (repo.value === null) {
487+
toast(`${label(current, hello.host)} isn't on GitHub`, {
488+
description: `${what} need a project with a GitHub remote.`,
489+
});
490+
}
483491
}
484492
const allProjects = all && page === "channels";
485493
const visible = current
@@ -603,6 +611,16 @@ export function App() {
603611
];
604612
});
605613
groups.unshift(pinnedGroup(channels, pins, hello, collapsed, attention, online));
614+
// An archived channel says to unarchive it from the sidebar, so All Projects lists yours too.
615+
const archived = channels.filter((value) =>
616+
value.state === "archived" && value.owner === hello.user
617+
).map((value) => row(value, hello.user, hello.host, online(value), attention.get(value.id)));
618+
groups.push({
619+
id: "archived",
620+
label: "Archived",
621+
rows: archived,
622+
collapsed: collapsed.archived,
623+
});
606624
}
607625

608626
function chooseProject(id: string) {
@@ -679,8 +697,18 @@ export function App() {
679697
if (value) remove(value);
680698
}
681699

700+
/** Opening and cloning go through the host, so a shortcut while disconnected says why it can't. */
701+
function disconnected(action: string) {
702+
toast.error(`Can't ${action} while disconnected`, {
703+
description: desktop
704+
? "Ace Helper isn't connected. Open Settings → This Mac to start it."
705+
: "Ace is reconnecting to your host.",
706+
});
707+
}
708+
682709
async function choose() {
683-
if (!connected || picking.current) return;
710+
if (!connected) return disconnected("open a folder");
711+
if (picking.current) return;
684712
if (!desktop) return setAdding(true);
685713
picking.current = true;
686714
try {
@@ -707,7 +735,8 @@ export function App() {
707735
}
708736

709737
function cloneUrl() {
710-
if (connected) setSetup({});
738+
if (!connected) return disconnected("clone a repository");
739+
setSetup({});
711740
}
712741

713742
function startSetup(project: AppProject, start?: { text?: string; onCreated?: () => void }) {

‎apps/app/src/dashboard.tsx‎

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -88,23 +88,27 @@ const PLACES = [
8888
},
8989
];
9090

91-
/** Teammates' pulse covers what ended since this project's dashboard was last opened here. */
92-
const FIRST_VISIT = 24 * 60 * 60_000;
91+
/**
92+
* Teammates' pulse covers what ended since this project's dashboard was last opened here, and at
93+
* least the last day, so a reload or a quick look doesn't clear what someone hasn't read yet.
94+
*/
95+
const DAY = 24 * 60 * 60_000;
9396

9497
function useLastVisit(project: string): number {
9598
const key = `ace:dashboard-visit:${host.url}:${project}`;
9699
const [since] = useState(() => {
100+
const day = Date.now() - DAY;
97101
try {
98-
return Number(localStorage.getItem(key)) || Date.now() - FIRST_VISIT;
102+
return Math.min(Number(localStorage.getItem(key)) || day, day);
99103
} catch {
100-
return Date.now() - FIRST_VISIT;
104+
return day;
101105
}
102106
});
103107
useEffect(() => {
104108
try {
105109
localStorage.setItem(key, String(Date.now()));
106110
} catch {
107-
// Without storage, every visit covers the last day.
111+
// Without storage, every visit covers only the last day.
108112
}
109113
}, [key]);
110114
return since;
@@ -209,6 +213,16 @@ function WorkRow({ work, owner, busy, onChannel, onStart }: {
209213
);
210214
}
211215

216+
const CLOCK = new Intl.DateTimeFormat(undefined, { timeStyle: "short" });
217+
const DATED = new Intl.DateTimeFormat(undefined, { dateStyle: "medium", timeStyle: "short" });
218+
219+
/** When a run ended: the time today, with the date before that. */
220+
function ended(at: number) {
221+
return new Date(at).toDateString() === new Date().toDateString()
222+
? CLOCK.format(at)
223+
: DATED.format(at);
224+
}
225+
212226
function TeammateRow({ teammate, onChannel }: {
213227
teammate: Teammate;
214228
onChannel: (channel: Listing) => void;
@@ -219,6 +233,7 @@ function TeammateRow({ teammate, onChannel }: {
219233
<span key={channel.id}>
220234
{index > 0 && ", "}
221235
<ChannelLink channel={channel} onChannel={onChannel} />
236+
{channel.ran && ` at ${ended(channel.ran.at)}`}
222237
</span>
223238
));
224239
return (

‎apps/app/src/navigation.tsx‎

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,8 @@ export function Navigation({ page, user, github, repo, onPage, onSettings }: {
2828
}) {
2929
return (
3030
<Nav
31-
footer={
32-
<UserMenu name={user || "Ace"} handle={github} side="right" onSettings={onSettings} />
33-
}
31+
// Until the host names its user, any initials would be a placeholder's.
32+
footer={user && <UserMenu name={user} handle={github} side="right" onSettings={onSettings} />}
3433
>
3534
<NavList>
3635
<NavListLink

‎apps/app/src/open-project.tsx‎

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { IconArrowDownToLine, IconBranch, IconFolderSearch } from "@ace/ui/icons
1414

1515
import { desktop } from "./desktop";
1616
import { host } from "./host";
17+
import { Copyable } from "./team-setup";
1718

1819
export function EmptyProjects({ onOpen, onClone, disabled }: {
1920
onOpen: () => void;
@@ -79,6 +80,10 @@ export function InitGit({ path, name, empty, ignored, disabled, onInit }: {
7980
}
8081
}
8182

83+
const folder = `'${path.replaceAll("'", `'\\''`)}'`;
84+
const commands = `cd ${folder} && ${
85+
empty ? "" : "git init && "
86+
}git add -A && git commit -m "Initial commit"`;
8287
const lanes = empty
8388
? `Channels work in lanes, which are Git worktrees, and ${name} has no commit yet.`
8489
: "Channels work in lanes, which are Git worktrees.";
@@ -92,14 +97,19 @@ export function InitGit({ path, name, empty, ignored, disabled, onInit }: {
9297
</h2>
9398
{ignored
9499
? (
95-
<p className="max-w-[30rem] text-pretty text-xs leading-5 text-muted-foreground">
96-
{lanes} {name} already has a{" "}
97-
<code>.gitignore</code>, so Ace won't commit under its rules. {empty ? "Commit" : (
98-
<>
99-
Run <code>git init</code> and commit
100-
</>
101-
)} its files yourself, then channels can start here.
102-
</p>
100+
<>
101+
<p className="max-w-[30rem] text-pretty text-xs leading-5 text-muted-foreground">
102+
{lanes} {name} already has a{" "}
103+
<code>.gitignore</code>, so Ace won't commit under its rules. {empty ? "Commit" : (
104+
<>
105+
Run <code>git init</code> and commit
106+
</>
107+
)} its files yourself, such as in a terminal, then channels can start here.
108+
</p>
109+
<div className="w-full max-w-[30rem] text-left">
110+
<Copyable value={commands} />
111+
</div>
112+
</>
103113
)
104114
: (
105115
<>

‎packages/ui/src/components/rich-text/commands.ts‎

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,24 @@ function codeBlockRule() {
217217
});
218218
}
219219

220-
/** Input rules for the Markdown list and inline code markers people type, as in Slack or Notion. */
220+
/**
221+
* `**bold**` or `~~strike~~` as its closing marker's last character is typed, which the rule
222+
* replaces; as in Markdown, the text can't start or end with a space.
223+
*/
224+
function pairRule(marker: "*" | "~", mark: typeof schema.marks.strong) {
225+
let m = `\\${marker}`;
226+
let pattern = new RegExp(`(?:^|[^${m}])(${m}${m}([^${m}\\s](?:[^${m}]*[^${m}\\s])?)${m}${m})$`);
227+
return new InputRule(pattern, (state, match, start, end) => {
228+
let from = start + match[0]!.length - match[1]!.length;
229+
return state.tr
230+
.delete(end - 1, end)
231+
.delete(from, from + 2)
232+
.addMark(from, end - 3, mark.create())
233+
.removeStoredMark(mark);
234+
}, { inCodeMark: false });
235+
}
236+
237+
/** Input rules for the Markdown markers people type, as in Slack or Notion. */
221238
function markdownRules() {
222239
return [
223240
wrappingInputRule(/^\s*([-+*])\s$/, schema.nodes.bullet_list),
@@ -236,6 +253,8 @@ function markdownRules() {
236253
.addMark(from, end - 1, code.create())
237254
.removeStoredMark(code);
238255
}, { inCodeMark: false }),
256+
pairRule("*", schema.marks.strong),
257+
pairRule("~", schema.marks.strike),
239258
];
240259
}
241260

0 commit comments

Comments
 (0)