Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
112 changes: 112 additions & 0 deletions packages/app/src/components/SkillsSidebarSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
RefreshCw,
} from 'lucide-react';
import { useEffect, useState } from 'react';
import { toast } from 'sonner';
import { NewSkillDialog } from '@/components/NewSkillDialog';
import { SkillStateBadge } from '@/components/SkillStateBadge';
import {
Expand Down Expand Up @@ -116,6 +117,9 @@ export function SkillsSidebarSection() {
<CollapsibleContent>
<SidebarGroupContent>
<SkillImportPrompt />
<SkillsShareLocalOnlyPrompt
hasProjectSkills={skills.some((s) => s.scope === 'project' && !s.managed)}
/>
{skills.length === 0 ? (
<p className="px-2 py-1 text-xs text-muted-foreground">
<Trans>No skills yet.</Trans>
Expand Down Expand Up @@ -498,6 +502,114 @@ function FileTreeNode({
);
}

/**
* Local-only skills-are-hidden prompt. In local-only mode the whole `.ok/`
* tree is git-ignored, so `.ok/skills/` (shareable content) can't be committed
* — the "skills silently can't be shared" trap. When project skills exist in
* that state, offer a one-click carve-out that makes JUST `.ok/skills/`
* shareable while the rest of `.ok/` stays local (via `sharing.setSkillsShared`).
* Desktop-only (the sharing bridge is absent on the web host).
*/
function SkillsShareLocalOnlyPrompt({ hasProjectSkills }: { hasProjectSkills: boolean }) {
const { t } = useLingui();
// `localOnlyHidden` = local-only AND skills not yet carved out (the broken
// state this prompt exists to fix). null = not yet read / not applicable.
const [localOnlyHidden, setLocalOnlyHidden] = useState<boolean | null>(null);
const [busy, setBusy] = useState(false);

useEffect(() => {
// useEffect runs client-side, so `window` is defined (oxlint no-restricted-syntax).
const bridge = window.okDesktop?.sharing;
if (!bridge) return;
let cancelled = false;
const read = () => {
void bridge
.status()
.then((s) => {
if (!cancelled) setLocalOnlyHidden(s.mode === 'local-only' && !s.skillsShared);
})
.catch((err) => {
// Advisory read — degrade to hiding the prompt, but keep a signal so an
// IPC regression isn't invisible (packaged desktop has no user console).
console.warn(
JSON.stringify({
event: 'skills-share-status-read-failed',
error: err instanceof Error ? err.message : String(err),
}),
);
});
};
read();
// Re-read on window refocus so the banner tracks a sharing-mode change made
// in Settings while the sidebar stayed mounted (closing that dialog refocuses
// the window). Without this the mount-time read goes stale.
window.addEventListener('focus', read);
return () => {
cancelled = true;
window.removeEventListener('focus', read);
};
}, []);

if (!hasProjectSkills || localOnlyHidden !== true) return null;

// No try/finally: this repo's React Compiler can't lower a finalizer clause,
// so capture the result/error and settle `busy` after the await (mirrors
// SharingSection's handler).
const share = async () => {
const bridge = window.okDesktop?.sharing;
if (!bridge || busy) return;
setBusy(true);
let result: Awaited<ReturnType<typeof bridge.setSkillsShared>> | null = null;
let err: unknown = null;
try {
result = await bridge.setSkillsShared(true);
} catch (caught) {
err = caught;
}
setBusy(false);
if (err !== null) {
// A wire-protocol violation surfaces as `ok:sharing:dispatch: ...` from the
// preload — meaningless to end users, so show a generic message instead.
const raw = err instanceof Error ? err.message : '';
toast.error(
raw.startsWith('ok:sharing:dispatch:')
? t`Couldn't share skills: internal error. Please restart the app.`
: raw || t`Couldn't share skills`,
);
return;
}
if (result === null) return;
if (result.kind === 'applied') {
toast.success(t`Skills are now shared. Commit .ok/skills to share them with your team.`);
setLocalOnlyHidden(false);
} else if (result.kind === 'no-exclude') {
// Map internal reason codes to plain English (mirrors SharingSection).
const detail =
result.reason === 'no-git'
? t`no git repository here`
: result.reason === 'inaccessible'
? t`the git exclude file isn't writable`
: t`git configuration is unavailable`;
toast.warning(t`Couldn't share skills — ${detail}.`);
}
};

return (
<div className="mx-2 mb-1 rounded-md border border-amber-300 bg-amber-50 p-2 text-xs dark:border-amber-700 dark:bg-amber-950">
<p className="mb-2 text-amber-900 dark:text-amber-200">
<Trans>
This project is local-only, so .ok/skills is hidden from git and these skills can't be
committed or shared. Share just the skills folder — the rest of your OK config stays on
this computer.
</Trans>
</p>
<Button size="sm" className="h-6 px-2 text-xs" disabled={busy} onClick={() => void share()}>
<Trans>Share skills</Trans>
</Button>
</div>
);
}

/**
* One-time import offer: when this project is undecided about skill management
* AND there are editor-dir skills OK could adopt, ask once. "Import" flips the
Expand Down
73 changes: 72 additions & 1 deletion packages/app/src/components/settings/SharingSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,14 @@ function SharingSectionBody() {
// renders (the toast already surfaced the real error). A later refresh
// failure keeps the last good status rather than clobbering it.
setStatus(
(prev) => prev ?? { kind: 'status', mode: 'no-git', excluded: [], trackedUpstream: [] },
(prev) =>
prev ?? {
kind: 'status',
mode: 'no-git',
excluded: [],
trackedUpstream: [],
skillsShared: false,
},
);
}
}
Expand Down Expand Up @@ -130,6 +137,43 @@ function SharingSectionBody() {
await refresh();
}

// Undo the local-only skills carve-out (`.ok/skills` back to hidden). Only
// offered when the project is in the carve state; mirrors onSelect's
// capture-error-after-await shape (no try/finally — React Compiler can't
// lower a finalizer).
async function onUndoSkillsShare() {
const bridge = window.okDesktop?.sharing;
if (!bridge || status === null || busy) return;
setBusy(true);
let result: Awaited<ReturnType<typeof bridge.setSkillsShared>> | null = null;
let err: unknown = null;
try {
result = await bridge.setSkillsShared(false);
} catch (caught) {
err = caught;
}
setBusy(false);
if (err !== null) {
toast.error(err instanceof Error ? err.message : t`Couldn't update skills sharing`);
return;
}
if (result === null) return;
if (result.kind === 'no-exclude') {
// Inspect the result like onSelect does — a `no-exclude` outcome (git
// config unavailable / exclude file unwritable) is a failure, not success.
const detail =
result.reason === 'no-git'
? t`no git repository here`
: result.reason === 'inaccessible'
? t`the git exclude file isn't writable`
: t`git configuration is unavailable`;
toast.warning(t`Couldn't update skills sharing — ${detail}.`);
return;
}
toast.success(t`.ok/skills is back to local-only.`);
await refresh();
}

if (status === null) {
return (
<section aria-labelledby={TITLE_ID} className="space-y-3">
Expand Down Expand Up @@ -215,6 +259,33 @@ function SharingSectionBody() {
</RadioGroup>
)}

{status.mode === 'local-only' && status.skillsShared ? (
<div
className="flex items-start justify-between gap-2 rounded-md border border-muted-foreground/30 bg-muted/40 p-3 text-sm"
data-testid="settings-sharing-skills-exception"
>
<span>
<span className="font-medium">
<Trans>Exception: skills are shared</Trans>
</span>
<span className="block text-1sm text-muted-foreground">
<Trans>
.ok/skills is committable so your team gets your skills; the rest of .ok/ stays on
this computer.
</Trans>
</span>
</span>
<Button
variant="outline"
size="sm"
disabled={busy}
onClick={() => void onUndoSkillsShare()}
>
<Trans>Undo</Trans>
</Button>
</div>
) : null}

{refusal !== null ? (
<div
className="space-y-2 rounded-md border border-destructive bg-destructive/10 p-3 text-sm"
Expand Down
4 changes: 4 additions & 0 deletions packages/app/src/lib/desktop-bridge-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -743,6 +743,8 @@ export interface OkSharingStatusResult {
readonly mode: 'shared' | 'local-only' | 'no-git';
readonly excluded: readonly string[];
readonly trackedUpstream: readonly string[];
/** True when local-only but `.ok/skills/` is carved back out as shareable. */
readonly skillsShared: boolean;
}

export type OkSharingSetModeResult =
Expand Down Expand Up @@ -1157,6 +1159,8 @@ export interface OkDesktopBridge {
sharing: {
status(): Promise<OkSharingStatusResult>;
setMode(mode: 'shared' | 'local-only'): Promise<OkSharingSetModeResult>;
/** Toggle `.ok/skills/` shareability within local-only mode. */
setSkillsShared(shared: boolean): Promise<OkSharingSetModeResult>;
};

/**
Expand Down
19 changes: 19 additions & 0 deletions packages/app/src/locales/en/messages.json
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,7 @@
"4buZWd": ["Inherited from ", ["path"]],
"4c_a6S": ["Nothing set yet."],
"4g19V6": ["Failed to update property"],
"4h3e0H": ["Couldn't update skills sharing — ", ["detail"], "."],
"4hJhzz": ["Table"],
"4ieVh2": ["Pick “Custom” above to use this palette."],
"4jDd0j": ["Anyone can see"],
Expand Down Expand Up @@ -474,6 +475,7 @@
"9oE4C3": ["Browse your repos:"],
"9sBe85": ["Follow us on"],
"9tJX8r": ["By meaning"],
"9uI_rE": ["Undo"],
"9xCnv8": ["Hide Terminal"],
"9zb2WA": ["Connecting"],
"A1taO8": ["Search"],
Expand Down Expand Up @@ -552,6 +554,9 @@
"Bdha5y": ["Publishing..."],
"BeWTQy": ["Loading starter packs"],
"BgV29F": ["Visual editor find input focused"],
"Bh9671": [
"This project is local-only, so .ok/skills is hidden from git and these skills can't be committed or shared. Share just the skills folder — the rest of your OK config stays on this computer."
],
"Bn9-El": ["default"],
"Bv5a9_": ["Selection statistics"],
"Bx4EXG": ["Actions for ", ["label"]],
Expand Down Expand Up @@ -655,6 +660,7 @@
],
"E7BhZn": ["Blank note"],
"E9bKSA": ["Creating"],
"EARn6h": ["Couldn't share skills — ", ["detail"], "."],
"EBVbtP": ["Could not duplicate item"],
"EDl9kS": ["Subscribe"],
"EDm3wJ": ["Loading diff renderer"],
Expand Down Expand Up @@ -902,8 +908,12 @@
"Jw6EC4": ["Branch switch failed — try opening the ", ["targetNoun"], " manually."],
"K-a33w": ["Start writing to create this page"],
"K-h9cT": ["Couldn't save the resolution for ", ["filePath"], "."],
"K0FgsP": [
".ok/skills is committable so your team gets your skills; the rest of .ok/ stays on this computer."
],
"K0N7zS": ["Config sharing status read failed"],
"K1ckLX": ["Failed to create folder"],
"K2v7QF": ["Couldn't share skills"],
"K6MQQk": ["Sync is failing. See the sync status for details."],
"K6jjWB": [["packName"], " was already set up. Nothing to do."],
"K6jxcj": ["Add a key"],
Expand Down Expand Up @@ -939,6 +949,7 @@
"> isn't a known component. Copy the source to use it elsewhere, or delete the block."
],
"Khxcyu": ["Terminal access isn't enabled for this project."],
"Kiytqd": ["Exception: skills are shared"],
"KkEAud": ["Create a file from the current document or folder context."],
"KmydK6": ["Bold"],
"KnoB--": ["Will be saved as <0>", ["sanitized"], "</0>."],
Expand Down Expand Up @@ -1180,6 +1191,7 @@
"PiH3UR": ["Copied!"],
"PkeYXL": ["Expanded graph mode"],
"Pkw7J9": ["This folder is empty."],
"PlGC6r": ["the git exclude file isn't writable"],
"PqYWiC": [
"Couldn't restart the server automatically. Try running `ok stop all` in a terminal, then reopen this project — or restart your computer if it persists."
],
Expand All @@ -1190,6 +1202,7 @@
"Q5fqMz": ["Connection error — try again"],
"Q6hhn8": ["Preferences"],
"Q7W-mC": ["Same folder as current file"],
"Q7cNKe": ["Skills are now shared. Commit .ok/skills to share them with your team."],
"Q8A-U_": ["Connected to"],
"QAi7Q9": [
"Branch <0>",
Expand Down Expand Up @@ -1305,6 +1318,7 @@
"TJnkyf": ["ok ui responded but server.lock has no port yet"],
"TKQ7K-": ["Install"],
"TLIO3j": ["Toggle italic formatting."],
"TUzThQ": ["Share skills"],
"T_R-Qz": ["Primary"],
"T_wG4n": ["We couldn't check your GitHub connection."],
"Ta25TG": ["No history yet"],
Expand Down Expand Up @@ -1504,6 +1518,7 @@
"XvPJaT": ["Diff layout"],
"Xv_ijG": ["https://github.com/owner/repo or owner/repo"],
"Xw_maT": ["Type a query, then press ↵ to search your pages by meaning."],
"Xwump4": ["no git repository here"],
"XyOUTJ": ["Exit merge"],
"Xzy0s0": ["Remove ", ["fullLabel"], " from context"],
"Y-Urt2": ["Block math equation rendered with KaTeX from a LaTeX source string."],
Expand Down Expand Up @@ -2310,6 +2325,7 @@
"ogAuyr": ["Back to document view"],
"ohUJJM": ["Plugins"],
"omXJcU": ["Close others"],
"onTlRk": ["Couldn't update skills sharing"],
"optX0N": [["0"], "h ago"],
"oqB2ez": ["Previous match"],
"osRqlv": ["Open delete confirmation for the selected file-tree items."],
Expand All @@ -2329,6 +2345,7 @@
"pEQFsW": ["No matching projects."],
"pFagOn": ["Source editing"],
"pHYP6v": ["Open <0>Customize</0> in the sidebar <1/> <2>Skills</2>."],
"pQf2Ux": [".ok/skills is back to local-only."],
"pRc-Im": ["Undone all edits on ", ["docName"]],
"pTPqMK": ["Toggle a fenced code block."],
"pWT04I": ["Checking..."],
Expand Down Expand Up @@ -2372,6 +2389,7 @@
"qSKu2v": ["Source navigation and selection"],
"qSxaOz": ["Accept their deletion"],
"qUB_o_": ["Auto-sync is off — your edits stay local until you commit and push manually."],
"qXN416": ["Couldn't share skills: internal error. Please restart the app."],
"qXg5W9": ["File saves to <0>~/Downloads/openknowledge.skill</0>."],
"qXsafh": ["Publish to GitHub"],
"qZv5ZR": ["Config sharing toggle failed"],
Expand Down Expand Up @@ -2762,6 +2780,7 @@
"The properties block at the top of this doc has a formatting error. Switch to source mode to fix it."
],
"zbymaY": [["0"], "m ago"],
"zcmYx4": ["git configuration is unavailable"],
"zd00pH": ["Copy Path"],
"zgSFyG": ["External link"],
"zkz0Fg": ["Set up syncing"],
Expand Down
Loading