Skip to content

Commit 402f1f5

Browse files
committed
fix(web): stop unreachable environments holding lists in loading
- Apply reachable-environment loading state to notes and automations - Polish Skills settings search, actions, and status display
1 parent 1701041 commit 402f1f5

5 files changed

Lines changed: 194 additions & 142 deletions

File tree

‎apps/mobile/src/state/notes.ts‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { useAtomValue } from "@effect/atom-react";
22
import { createNotesEnvironmentAtoms } from "@t3tools/client-runtime/state/notes";
33
import type { EnvironmentId, NoteSummary } from "@t3tools/contracts";
4+
import { isAnswerExpected } from "@t3tools/client-runtime/connection";
45
import * as Option from "effect/Option";
56
import { AsyncResult, Atom } from "effect/unstable/reactivity";
67
import { connectionAtomRuntime } from "../connection/runtime";
@@ -15,11 +16,11 @@ const allNotesAtom = Atom.family((query: string) =>
1516
Atom.make((get) => {
1617
const notes: EnvironmentNote[] = [];
1718
let isPending = false;
18-
for (const environmentId of get(environmentPresentations.presentationsAtom).keys()) {
19+
for (const [environmentId, presentation] of get(environmentPresentations.presentationsAtom)) {
1920
const result = get(notesEnvironment.list({ environmentId, input: query ? { query } : {} }));
2021
const value = Option.getOrNull(AsyncResult.value(result));
2122
if (value === null) {
22-
isPending ||= result._tag !== "Failure";
23+
isPending ||= result._tag !== "Failure" && isAnswerExpected(presentation);
2324
continue;
2425
}
2526
notes.push(...value.notes.map((note) => ({ ...note, environmentId })));

‎apps/web/src/components/settings/SkillsSettings.tsx‎

Lines changed: 174 additions & 135 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
import { normalizeSkillVisibilityPath } from "@t3tools/client-runtime/providerSkills";
2-
import { CopyIcon, EyeIcon, FolderOpenIcon, RefreshCwIcon } from "lucide-react";
2+
import {
3+
CopyIcon,
4+
EyeIcon,
5+
FolderOpenIcon,
6+
PlusIcon,
7+
SearchIcon,
8+
TriangleAlertIcon,
9+
} from "lucide-react";
310
import { useEffect, useState } from "react";
411

512
import { useProjectFileQuery } from "../files/projectFilesQueryState";
@@ -12,9 +19,11 @@ import { useAtomCommand } from "../../state/use-atom-command";
1219
import { Badge } from "../ui/badge";
1320
import { Button } from "../ui/button";
1421
import { Dialog, DialogHeader, DialogPopup, DialogTitle } from "../ui/dialog";
15-
import { Input } from "../ui/input";
22+
import { InputGroup, InputGroupAddon, InputGroupInput } from "../ui/input-group";
23+
import { RefreshIcon } from "../ui/refresh-icon";
1624
import { toastManager } from "../ui/toast";
17-
import { SettingsPageContainer } from "./settingsLayout";
25+
import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip";
26+
import { SettingsPageContainer, SettingsSection } from "./settingsLayout";
1827
import { useSettingsScope } from "./SettingsScopeContext";
1928
import {
2029
discoverLocalSkillFiles,
@@ -109,158 +118,188 @@ export function SkillsSettings() {
109118

110119
return (
111120
<SettingsPageContainer width="wide">
112-
<section className="flex flex-col gap-2 px-3 sm:px-4">
113-
<h1 className="text-2xl font-semibold">Skills</h1>
114-
<p className="text-sm text-muted-foreground">
115-
File skills in your home folders and selected projects.
116-
</p>
117-
</section>
118-
<div className="flex items-center gap-3 px-3 sm:px-4">
119-
<span className="shrink-0 text-sm tabular-nums text-muted-foreground">
120-
{filtered.length} {filtered.length === 1 ? "skill" : "skills"}
121-
</span>
122-
<Input
123-
nativeInput
124-
type="search"
125-
value={query}
126-
onChange={(event) => setQuery(event.currentTarget.value)}
127-
placeholder="Filter"
128-
aria-label="Filter skills"
129-
className="min-w-0 flex-1"
130-
/>
131-
<Button
132-
type="button"
133-
size="icon-sm"
134-
variant="ghost"
135-
aria-label="Refresh skills"
136-
disabled={refreshing}
137-
onClick={() => setRevision((value) => value + 1)}
138-
>
139-
<RefreshCwIcon className={refreshing ? "animate-spin" : undefined} />
140-
</Button>
141-
<Button
142-
type="button"
143-
variant="outline"
144-
size="sm"
145-
disabled={targets.length === 0}
146-
onClick={() => setAdding(true)}
147-
>
148-
Add skill
149-
</Button>
150-
</div>
151-
{errors.length > 0 && (
152-
<p role="alert" className="px-4 text-sm text-destructive">
153-
Some skills could not be loaded. {errors.join("; ")}
154-
</p>
155-
)}
156-
<p className="px-4 text-xs text-muted-foreground">
157-
Switches show or hide skills in this client's T3 skill pickers. Providers manage automatic
158-
skill discovery.
159-
</p>
160-
<div className="overflow-hidden rounded-xl border">
121+
<SettingsSection
122+
title="Skills"
123+
headerAction={
124+
<div className="flex items-center gap-1.5">
125+
<span className="text-2xs tabular-nums text-muted-foreground">
126+
{filtered.length} {filtered.length === 1 ? "skill" : "skills"}
127+
</span>
128+
<InputGroup className="w-48">
129+
<InputGroupAddon>
130+
<SearchIcon aria-hidden className="size-3" />
131+
</InputGroupAddon>
132+
<InputGroupInput
133+
type="search"
134+
size="sm"
135+
value={query}
136+
onChange={(event) => setQuery(event.currentTarget.value)}
137+
onKeyDown={(event) => {
138+
if (event.key === "Escape") setQuery("");
139+
}}
140+
placeholder="Search skills"
141+
aria-label="Search skills"
142+
/>
143+
</InputGroup>
144+
<Tooltip>
145+
<TooltipTrigger
146+
render={
147+
<Button
148+
type="button"
149+
size="icon-xs"
150+
variant="ghost-muted"
151+
aria-label="Refresh skills"
152+
disabled={refreshing}
153+
onClick={() => setRevision((value) => value + 1)}
154+
>
155+
<RefreshIcon refreshing={refreshing} />
156+
</Button>
157+
}
158+
/>
159+
<TooltipPopup side="top">Refresh skills</TooltipPopup>
160+
</Tooltip>
161+
<Tooltip>
162+
<TooltipTrigger
163+
render={
164+
<Button
165+
type="button"
166+
size="icon-xs"
167+
variant="ghost-muted"
168+
aria-label="Add skill"
169+
disabled={targets.length === 0}
170+
onClick={() => setAdding(true)}
171+
>
172+
<PlusIcon />
173+
</Button>
174+
}
175+
/>
176+
<TooltipPopup side="top">Add skill</TooltipPopup>
177+
</Tooltip>
178+
</div>
179+
}
180+
>
181+
{errors.map((error) => (
182+
<p
183+
key={error}
184+
role="alert"
185+
className="flex items-center gap-2 px-4 py-2.5 text-xs text-warning-foreground"
186+
>
187+
<TriangleAlertIcon aria-hidden className="size-3.5 shrink-0 text-warning" />
188+
{error}
189+
</p>
190+
))}
161191
{filtered.length === 0 ? (
162192
<p className="px-4 py-8 text-center text-sm text-muted-foreground">
163193
{refreshing
164194
? "Loading skills..."
165195
: normalizedQuery
166196
? "No matching skills"
167-
: "No file skills found for this selection"}
197+
: "No skills in your home folders or selected projects"}
168198
</p>
169199
) : (
170-
filtered.map((skill) => (
171-
<div
172-
key={JSON.stringify([skill.environmentId, skill.path])}
173-
className="flex flex-col gap-1 border-b px-4 py-3 last:border-b-0"
174-
>
175-
<div className="flex min-w-0 items-center gap-2">
176-
<button
177-
type="button"
178-
className="min-w-0 flex-1 truncate text-left text-sm font-medium hover:underline"
179-
onClick={() => setPreview(skill)}
180-
>
181-
{skill.name}
182-
</button>
183-
<Badge variant="secondary" size="sm">
184-
{skill.scope === "project" ? "Project" : "Personal"}
185-
</Badge>
186-
<span className="w-20 shrink-0 truncate text-right text-xs text-muted-foreground">
187-
{skill.source}
188-
</span>
189-
<Switch
190-
size="sm"
191-
checked={
192-
!skill.aliases.some((alias) =>
193-
(hiddenByEnvironment[skill.environmentId] ?? []).some(
194-
(hidden) =>
195-
normalizeSkillVisibilityPath(hidden) ===
196-
normalizeSkillVisibilityPath(alias),
197-
),
198-
)
199-
}
200-
aria-label={`Show ${skill.name} in skill pickers`}
201-
onCheckedChange={(checked) =>
202-
setVisible(skill.environmentId, skill.aliases, checked)
203-
}
204-
/>
205-
</div>
206-
{skill.description ? (
207-
<p className="truncate text-sm text-muted-foreground">{skill.description}</p>
208-
) : null}
209-
<div className="flex min-w-0 items-center gap-1">
210-
<p className="min-w-0 flex-1 truncate text-xs text-muted-foreground">
211-
{skill.path}
212-
</p>
213-
{multipleEnvironments ? (
214-
<span className="max-w-24 truncate text-xs text-muted-foreground">
215-
{skill.environmentLabel}
200+
filtered.map((skill) => {
201+
const visible = !skill.aliases.some((alias) =>
202+
(hiddenByEnvironment[skill.environmentId] ?? []).some(
203+
(hidden) =>
204+
normalizeSkillVisibilityPath(hidden) === normalizeSkillVisibilityPath(alias),
205+
),
206+
);
207+
return (
208+
<div
209+
key={JSON.stringify([skill.environmentId, skill.path])}
210+
className="flex flex-col gap-1 px-4 py-3"
211+
>
212+
<div className="flex min-w-0 items-center gap-2">
213+
<button
214+
type="button"
215+
className="min-w-0 flex-1 truncate text-left text-sm font-medium hover:underline"
216+
onClick={() => setPreview(skill)}
217+
>
218+
{skill.name}
219+
</button>
220+
<Badge variant="secondary" size="sm">
221+
{skill.scope === "project" ? "Project" : "Personal"}
222+
</Badge>
223+
<span className="w-20 shrink-0 truncate text-right text-xs text-muted-foreground">
224+
{skill.source}
216225
</span>
226+
<Tooltip>
227+
<TooltipTrigger
228+
render={
229+
<Switch
230+
size="sm"
231+
checked={visible}
232+
aria-label={`Show ${skill.name} in skill pickers`}
233+
onCheckedChange={(checked) =>
234+
setVisible(skill.environmentId, skill.aliases, checked)
235+
}
236+
/>
237+
}
238+
/>
239+
<TooltipPopup side="top">
240+
{visible ? "Shown in skill pickers" : "Hidden from skill pickers"}
241+
</TooltipPopup>
242+
</Tooltip>
243+
</div>
244+
{skill.description ? (
245+
<p className="truncate text-sm text-muted-foreground">{skill.description}</p>
217246
) : null}
218-
<Button
219-
type="button"
220-
size="icon-micro"
221-
variant="ghost-muted"
222-
aria-label={`Preview ${skill.name}`}
223-
onClick={() => setPreview(skill)}
224-
>
225-
<EyeIcon />
226-
</Button>
227-
<Button
228-
type="button"
229-
size="icon-micro"
230-
variant="ghost-muted"
231-
aria-label={`Copy path of ${skill.name}`}
232-
onClick={() => {
233-
void writeTextToClipboard(skill.path).catch(() =>
234-
toastManager.add({ type: "error", title: "Could not copy skill path" }),
235-
);
236-
}}
237-
>
238-
<CopyIcon />
239-
</Button>
240-
{revealableEnvironments.has(skill.environmentId) ? (
247+
<div className="flex min-w-0 items-center gap-1">
248+
<p className="min-w-0 flex-1 truncate text-xs text-muted-foreground">
249+
{skill.path}
250+
</p>
251+
{multipleEnvironments ? (
252+
<span className="max-w-24 truncate text-xs text-muted-foreground">
253+
{skill.environmentLabel}
254+
</span>
255+
) : null}
241256
<Button
242257
type="button"
243258
size="icon-micro"
244259
variant="ghost-muted"
245-
aria-label={`Reveal ${skill.name} in file manager`}
260+
aria-label={`Preview ${skill.name}`}
261+
onClick={() => setPreview(skill)}
262+
>
263+
<EyeIcon />
264+
</Button>
265+
<Button
266+
type="button"
267+
size="icon-micro"
268+
variant="ghost-muted"
269+
aria-label={`Copy path of ${skill.name}`}
246270
onClick={() => {
247-
void openInEditor({
248-
environmentId: skill.environmentId,
249-
input: { cwd: skill.path, editor: "file-manager", reveal: true },
250-
}).then((result) => {
251-
if (result._tag === "Failure")
252-
toastManager.add({ type: "error", title: "Could not reveal skill" });
253-
});
271+
void writeTextToClipboard(skill.path).catch(() =>
272+
toastManager.add({ type: "error", title: "Could not copy skill path" }),
273+
);
254274
}}
255275
>
256-
<FolderOpenIcon />
276+
<CopyIcon />
257277
</Button>
258-
) : null}
278+
{revealableEnvironments.has(skill.environmentId) ? (
279+
<Button
280+
type="button"
281+
size="icon-micro"
282+
variant="ghost-muted"
283+
aria-label={`Reveal ${skill.name} in file manager`}
284+
onClick={() => {
285+
void openInEditor({
286+
environmentId: skill.environmentId,
287+
input: { cwd: skill.path, editor: "file-manager", reveal: true },
288+
}).then((result) => {
289+
if (result._tag === "Failure")
290+
toastManager.add({ type: "error", title: "Could not reveal skill" });
291+
});
292+
}}
293+
>
294+
<FolderOpenIcon />
295+
</Button>
296+
) : null}
297+
</div>
259298
</div>
260-
</div>
261-
))
299+
);
300+
})
262301
)}
263-
</div>
302+
</SettingsSection>
264303
{adding && (
265304
<AddSkillDialog
266305
targets={targets}

0 commit comments

Comments
 (0)