Compare commits

..
9 Commits
Author SHA1 Message Date
adrianbonpin 01352ab3da docs: the alert shows size and age, not the old line count
The code never reads a file it is about to replace just to count its lines: stat
gives the size and the age, and the new content's line count is known without
reading anything. The spec overpromised the old count.
2026-10-07 17:03:29 +08:00
adrianbonpin adfd78e4eb docs: what file access reaches, and what it costs
Replaces the promise that Lam does not touch your files, which stopped being true,
and says what replaced it: the roots, the approval dialog, the per-answer ledger,
the denylist, and the one disclosure the dialog cannot make on the user's behalf,
which is that every byte read leaves the machine in a model request.
2026-10-07 17:01:58 +08:00
adrianbonpin bd6a71a9d0 feat: tell the model what its file tools reach, and wire them up
The runtime paragraph gains the tools, the roots and the approval rule when file
access is on, and keeps the old claim that Lam cannot read files when it is off,
because a model told the wrong thing about itself states it confidently.

The hook builds the policy and the ledger per turn, which is what makes the
approval last one answer rather than one session.
2026-10-07 17:01:28 +08:00
adrianbonpin 36d7d2fdea feat: file access preferences
Three settings: the switch, and the two root fields. Off by default, because the
read half of this feature cannot ask before it happens, and `ipGeolocation` in this
repository is already off for the weaker version of the same reason.

The manifest keeps the file's own formatting rather than prettier's, since the
repository's format script has never covered package.json.
2026-10-07 17:00:52 +08:00
adrianbonpin f42a828360 feat: seven file tools behind the policy and an approval
The file tools are absent from the list unless file access is on, which is the
difference between a model that cannot call them and one that is told not to. Each
handler is thin: argument checks here, and the operation itself in files.ts.

move_path reads `from`, not the shared path helper, which is the bug the tests
caught: an empty source path would have been refused by the policy on every move.
2026-10-07 17:00:08 +08:00
adrianbonpin f8acfce792 feat: the approval dialog and its per-answer ledger
The wording lives beside the dialog rather than beside the paths, which is what
keeps files.ts free of UI and lets every refusal path be tested without a
launcher. Vicinae's rememberUserChoice is declared in the API but not implemented
in the server, so the ledger is the only memory and it lasts one answer.

A dialog that cannot be shown is a refusal, not consent.
2026-10-07 16:59:06 +08:00
adrianbonpin 674327ec20 feat: the file path policy and its operations
One module with no Vicinae import, so the whole boundary tests headlessly: the
roots, the credential denylist, resolution that follows symlinks before judging
containment, and the seven operations, each returning a result rather than
throwing, so a refusal and an I/O failure reach the model in the same shape.

The mutating operations take an approval callback rather than importing a dialog,
which is what keeps this file free of UI and lets every refusal path be tested.

52 tests, including a symlink out of a root, a sibling whose name merely starts
like a root, and a temporary home with its own symlinks resolved first, because
mkdtemp on macOS hands back a path under /var.
2026-10-07 16:58:43 +08:00
adrianbonpin c9036389e3 docs: implementation plan for file system access
Ten tasks: the path policy and its operations, the approval dialog and its ledger,
seven tools, three preferences, what the model is told, the hook and setup screen
wiring, the README, and a verification task that mutation checks the policy rather
than trusting the suite.
2026-10-07 16:57:22 +08:00
adrianbonpin 2b09cc84e9 docs: spec file system access for Lam
Seven file tools behind a path policy and an approval step, with the platform
findings that shaped it: extensions are unsandboxed Node, confirmAlert exists,
rememberUserChoice does not, and environment.supportPath is the one directory an
extension is invited to write to.

Records the decisions taken with the user (read, write, edit and destructive
tools; one approval per path per answer; home minus a credential denylist; extra
roots in preferences) and the assumptions that follow from them, including the
two that go beyond the questions asked: delete and move asking every time, and
file access being off until switched on.
2026-10-07 16:51:41 +08:00
14 changed files with 44 additions and 41 deletions
+16 -16
View File
@@ -68,12 +68,12 @@ npm run build
Setup is two prompts on first launch, then one command you can revisit any time. Setup is two prompts on first launch, then one command you can revisit any time.
1. **The API key.** Vicinae prompts for required preferences before a command runs, so the very first launch asks for your Ollama API key. 1. **The API key.** Vicinae prompts for required preferences before a command runs, so the very first launch asks for your Ollama API key.
2. **The model.** The next prompt asks which model to use. Type it exactly, e.g. `deepseek-v4.1-flash:cloud` or `gpt-oss:120b`. Typing it by hand is the fragile part, so run **Manage Models** afterwards and pick the real one from the list. 2. **The model.** The next prompt asks which model to use. Type it exactly, e.g. `deepseek-v4.1-flash:cloud` or `gpt-oss:120b`. Typing it by hand is the fragile part, so run **Ollama Models** afterwards and pick the real one from the list.
3. **`Setup & Status`** verifies the connection, lists every model your key can actually reach, and reports which one is in use and where that came from. 3. **`Set Up Lam Agent`** verifies the connection, lists every model your key can actually reach, and reports which one is in use and where that came from.
To change the model later, run **Manage Models** and pick one, or edit the **Model** preference directly. To change the model later, run **Ollama Models** and pick one, or edit the **Model** preference directly.
> An extension can read preferences but not write them, which is why a pick made in **Manage Models** is stored by the extension and layered over the preference rather than saved into it. See [Models](#models) for how the two are reconciled, and for what a reset does. > An extension can read preferences but not write them, which is why a pick made in **Ollama Models** is stored by the extension and layered over the preference rather than saved into it. See [Models](#models) for how the two are reconciled, and for what a reset does.
## Keyboard shortcuts ## Keyboard shortcuts
@@ -83,7 +83,7 @@ To change the model later, run **Manage Models** and pick one, or edit the **Mod
| `↩` with an empty bar | Open the highlighted conversation | | `↩` with an empty bar | Open the highlighted conversation |
| `⌘ N` | New chat | | `⌘ N` | New chat |
| `⌘ ⇧ S` | Snip a region and attach it to this chat (only with a vision model) | | `⌘ ⇧ S` | Snip a region and attach it to this chat (only with a vision model) |
| `⌘ ⇧ ⌫` | Delete the highlighted conversation (also in **Chat History**) | | `⌘ ⇧ ⌫` | Delete the highlighted conversation (also in **Chat Sessions**) |
Prompts are single line. An earlier `⌘ ↩` multi-line composer was removed. See [Architecture](#architecture) for why a `Form.TextArea` cannot be laid out cleanly in a launcher. The `Tab` shortcut went with it: the left column *is* the conversation switcher now, so an accessory dropdown would only duplicate it. Prompts are single line. An earlier `⌘ ↩` multi-line composer was removed. See [Architecture](#architecture) for why a `Form.TextArea` cannot be laid out cleanly in a launcher. The `Tab` shortcut went with it: the left column *is* the conversation switcher now, so an accessory dropdown would only duplicate it.
@@ -118,16 +118,16 @@ vicinae 'vicinae://launch/lam-agent/chat?fallbackText=summarise%20this'
| --- | --- | | --- | --- |
| **Chat with Lam** | The chat surface. Bind this to your global shortcut. | | **Chat with Lam** | The chat surface. Bind this to your global shortcut. |
| **Snip to Lam** | Capture a region and start a new chat about it. A `no-view` command, so the launcher window closes before the selector appears. Bind this to a second shortcut for one-key "snip and ask". | | **Snip to Lam** | Capture a region and start a new chat about it. A `no-view` command, so the launcher window closes before the selector appears. Bind this to a second shortcut for one-key "snip and ask". |
| **Chat History** | Browse, reopen, and delete past conversations. | | **Chat Sessions** | Browse, reopen, and delete past conversations. |
| **Manage Models** | Browse the models the configured host can reach, pick the one Lam uses, and pull or delete models on a server you run. See [Models](#models). | | **Ollama Models** | Browse the models the configured host can reach, pick the one Lam uses, and pull or delete models on a server you run. See [Models](#models). |
| **Setup & Status** | Verify the connection, list available models, check whether the selected model can see images, and clear out chats or unattached photos. | | **Set Up Lam Agent** | Verify the connection, list available models, check whether the selected model can see images, and clear out chats or unattached photos. |
## Configuration ## Configuration
| Preference | Default | Notes | | Preference | Default | Notes |
| --- | --- | --- | | --- | --- | --- |
| **Ollama API Key** | None | Required. Stored by Vicinae as a password preference. | | **Ollama API Key** | None | Required. Stored by Vicinae as a password preference. |
| **Model** | None | Required. Any model your key can reach. **Manage Models** can pick one for you, and a pick takes precedence over this value until you reset it from the setup screen. | | **Model** | None | Required. Any model your key can reach. **Ollama Models** can pick one for you, and a pick takes precedence over this value until you reset it from the setup screen. |
| **Ollama Host** | `https://ollama.com` | Set to `http://localhost:11434` for a local server. | | **Ollama Host** | `https://ollama.com` | Set to `http://localhost:11434` for a local server. |
| **Location** | None | The place Lam should treat as yours, for example `Cebu City, Philippines`. Optional. The only exact location available, and the only one that stops anything being inferred. | | **Location** | None | The place Lam should treat as yours, for example `Cebu City, Philippines`. Optional. The only exact location available, and the only one that stops anything being inferred. |
| **IP Geolocation** | off | Off by default. When on, and only when **Location** is empty, your IP is sent to ipwho.is to resolve a rough area. | | **IP Geolocation** | off | Off by default. When on, and only when **Location** is empty, your IP is sent to ipwho.is to resolve a rough area. |
@@ -145,7 +145,7 @@ vicinae 'vicinae://launch/lam-agent/chat?fallbackText=summarise%20this'
## Models ## Models
**Manage Models** lists everything the configured host can reach, filtered by the search bar. Each row shows the parameter count and how much disk it takes, with tags for the model in use and for anything that reports image input. Picking one writes it to LocalStorage and `loadPreferences` layers it over the `Model` preference, so it takes effect on the next question with no restart. **Ollama Models** lists everything the configured host can reach, filtered by the search bar. Each row shows the parameter count and how much disk it takes, with tags for the model in use and for anything that reports image input. Picking one writes it to LocalStorage and `loadPreferences` layers it over the `Model` preference, so it takes effect on the next question with no restart.
| Action | Notes | | Action | Notes |
| --- | --- | | --- | --- |
@@ -211,7 +211,7 @@ If you turn on **IP Geolocation**, and only then, `get_location` resolves a roug
**It is coarser than "city-level" suggests, and that is worth knowing before trusting it.** An IP address identifies the point where the traffic left the ISP, not the person. Measured from one machine on Globe Telecom, an IPv4 lookup resolved to Cebu City and an IPv6 lookup moments later resolved to Talisay in Calabarzon, roughly 500km apart. If you want a place you can rely on, set **Location** and turn this off. **It is coarser than "city-level" suggests, and that is worth knowing before trusting it.** An IP address identifies the point where the traffic left the ISP, not the person. Measured from one machine on Globe Telecom, an IPv4 lookup resolved to Cebu City and an IPv6 lookup moments later resolved to Talisay in Calabarzon, roughly 500km apart. If you want a place you can rely on, set **Location** and turn this off.
The answer is cached for 6 hours rather than looked up per question, because an area does not change between questions and every lookup hands over another copy of your address. `Setup & Status` clears that cache, and the capability cache, when you re-test. The answer is cached for 6 hours rather than looked up per question, because an area does not change between questions and every lookup hands over another copy of your address. `Set Up Lam Agent` clears that cache, and the capability cache, when you re-test.
This extension does not use GPS. That would mean CoreLocation on macOS, which has no command-line interface, or GeoClue on Linux, whose usual provider resolves nearby Wi-Fi networks through a Mozilla service anyway. Neither is worth it for "is it late where you are". This extension does not use GPS. That would mean CoreLocation on macOS, which has no command-line interface, or GeoClue on Linux, whose usual provider resolves nearby Wi-Fi networks through a Mozilla service anyway. Neither is worth it for "is it late where you are".
@@ -287,7 +287,7 @@ The index entry for a conversation carries its image paths (`SessionSummary.atta
### Doing it by hand ### Doing it by hand
**Setup & Status** has a Maintenance section with the same two jobs as explicit actions: **Set Up Lam Agent** has a Maintenance section with the same two jobs as explicit actions:
| Action | What it deletes | | Action | What it deletes |
| --- | --- | | --- | --- |
@@ -352,8 +352,8 @@ The list shows the sources the answer cited, falling back to everything the turn
src/ src/
chat.tsx # "chat" command entrypoint, thin wrapper chat.tsx # "chat" command entrypoint, thin wrapper
snip.tsx # "snip" command entrypoint, no-view: captures then hands over snip.tsx # "snip" command entrypoint, no-view: captures then hands over
manage-models.tsx # "manage-models" command: browse, pick, pull, delete models.tsx # "models" command: browse, pick, pull, delete
chat-history.tsx # "chat-history" command: browse, reopen, delete conversations sessions.tsx # browse, reopen, delete conversations
setup.tsx # connection check + live model list setup.tsx # connection check + live model list
lib/ lib/
ollama.ts # the only file that talks HTTP ollama.ts # the only file that talks HTTP
@@ -423,7 +423,7 @@ The rendered height stays capped at 200px whatever is asked for (`Math.min(_rawH
**The logic layer has no Vicinae imports.** `agent.ts`, `ollama.ts`, `tools.ts` and friends are plain TypeScript, which is why they unit test without a runtime. A test enforces this. **The logic layer has no Vicinae imports.** `agent.ts`, `ollama.ts`, `tools.ts` and friends are plain TypeScript, which is why they unit test without a runtime. A test enforces this.
**Preferences have exactly one reader.** `loadPreferences` applies the model chosen in **Manage Models** on top of what Vicinae holds, and it is asynchronous because that pick lives in LocalStorage. `getLamPreferences` is deliberately not exported, because a second, synchronous reader would look correct and silently ignore the pick. Views take preferences through `hooks/usePreferences`, and the chat view is split in two so that `useChat` is only ever created once the model is known, rather than being pinned to whatever the preference said a moment earlier. **Preferences have exactly one reader.** `loadPreferences` applies the model chosen in **Ollama Models** on top of what Vicinae holds, and it is asynchronous because that pick lives in LocalStorage. `getLamPreferences` is deliberately not exported, because a second, synchronous reader would look correct and silently ignore the pick. Views take preferences through `hooks/usePreferences`, and the chat view is split in two so that `useChat` is only ever created once the model is known, rather than being pinned to whatever the preference said a moment earlier.
**Sessions are capped twice over**, once per turn and once per session, because `LocalStorage` has no documented ceiling. **Sessions are capped twice over**, once per turn and once per session, because `LocalStorage` has no documented ceiling.
@@ -441,7 +441,7 @@ The hand-off goes through `snip` specifically rather than straight to `chat` on
**An attached image is sent once and never replayed.** It is the largest thing a request can carry, so `buildMessages` attaches it to the question it belongs to and drops it afterwards, exactly as it already drops tool output. The cost is that a follow-up about a screenshot needs the image attached again; the alternative is re-sending every screenshot in the conversation on every turn. Only the path and mime type go into storage: `MAX_STORED_CHARS_PER_TURN` exists so one turn cannot dominate `LocalStorage`, and base64 would blow past it on a single 1080p capture. **An attached image is sent once and never replayed.** It is the largest thing a request can carry, so `buildMessages` attaches it to the question it belongs to and drops it afterwards, exactly as it already drops tool output. The cost is that a follow-up about a screenshot needs the image attached again; the alternative is re-sending every screenshot in the conversation on every turn. Only the path and mime type go into storage: `MAX_STORED_CHARS_PER_TURN` exists so one turn cannot dominate `LocalStorage`, and base64 would blow past it on a single 1080p capture.
**Whether the model can see is a tri-state, and the prompt gets the whole answer.** `canSeeImages` stays fail-closed, because offering the snip action to a model that cannot read would waste the selection gesture. But the system prompt needs `"unknown"` kept apart from `"no"`: telling a model it cannot see when the check merely failed is a false claim about the user's setup, and telling it nothing is what made it reason about its own capability instead of describing the image it had been sent. A model name that does not exist on the server is the common way to end up at `"unknown"`, which is one more reason the Manage Models command picks names from the list rather than taking them typed. **Whether the model can see is a tri-state, and the prompt gets the whole answer.** `canSeeImages` stays fail-closed, because offering the snip action to a model that cannot read would waste the selection gesture. But the system prompt needs `"unknown"` kept apart from `"no"`: telling a model it cannot see when the check merely failed is a false claim about the user's setup, and telling it nothing is what made it reason about its own capability instead of describing the image it had been sent. A model name that does not exist on the server is the common way to end up at `"unknown"`, which is one more reason the Models command picks names from the list rather than taking them typed.
## Development ## Development
+10 -7
View File
@@ -11,31 +11,34 @@
{ {
"name": "chat", "name": "chat",
"title": "Chat with Lam", "title": "Chat with Lam",
"subtitle": "Lam Agent",
"description": "Ask Lam anything. Answers stream in as they are generated, your session history is kept, and Lam can search the web when it needs current information.", "description": "Ask Lam anything. Answers stream in as they are generated, your session history is kept, and Lam can search the web when it needs current information.",
"mode": "view" "mode": "view"
}, },
{ {
"name": "snip", "name": "snip",
"title": "Snip to Lam", "title": "Snip to Lam",
"subtitle": "Lam Agent",
"description": "Capture a region of the screen and start a new chat about it. The launcher window closes first so it cannot end up in the shot. Offered only when the model in preferences supports image input.", "description": "Capture a region of the screen and start a new chat about it. The launcher window closes first so it cannot end up in the shot. Offered only when the model in preferences supports image input.",
"mode": "no-view" "mode": "no-view"
}, },
{ {
"name": "chat-history", "name": "sessions",
"title": "Chat History", "title": "Chat Sessions",
"description": "Browse, reopen, and clean up your past conversations with Lam.", "description": "Browse, reopen, and clean up your past conversations with Lam.",
"mode": "view" "mode": "view"
}, },
{ {
"name": "manage-models", "name": "models",
"title": "Manage Models", "title": "Ollama Models",
"subtitle": "Lam Agent",
"description": "Browse the models your Ollama host can reach, pick the one Lam should use, unload one that is holding memory, and pull or delete models on a server you run. A pick takes effect immediately; a pull keeps downloading until it finishes, so leave the launcher open.", "description": "Browse the models your Ollama host can reach, pick the one Lam should use, unload one that is holding memory, and pull or delete models on a server you run. A pick takes effect immediately; a pull keeps downloading until it finishes, so leave the launcher open.",
"mode": "view" "mode": "view"
}, },
{ {
"name": "setup", "name": "setup",
"title": "Setup & Status", "title": "Set Up Lam Agent",
"description": "Check your Ollama connection, pick the model Lam should use, tune how Lam answers, and clear out old chats or screenshots you never sent.", "description": "Check your Ollama connection, pick the model Lam should use, and tune how Lam answers.",
"mode": "view" "mode": "view"
} }
], ],
@@ -50,7 +53,7 @@
{ {
"name": "model", "name": "model",
"title": "Model", "title": "Model",
"description": "The model Lam should use, for example deepseek-v4.1-flash:cloud or gpt-oss:120b. Run \"Setup & Status\" to pick from the models your key can access.", "description": "The model Lam should use, for example deepseek-v4.1-flash:cloud or gpt-oss:120b. Run \"Set Up Lam Agent\" to pick from the models your key can access.",
"type": "textfield", "type": "textfield",
"required": true "required": true
}, },
+1 -1
View File
@@ -191,7 +191,7 @@ describe("resolveStartup", () => {
}); });
it("does not resume when a session was pushed in by hand", async () => { it("does not resume when a session was pushed in by hand", async () => {
// The Chat History command already knows which conversation to show. // The sessions command already knows which conversation to show.
const p = ports(); const p = ports();
await resolveStartup(input({ hasInitialSession: true }), p); await resolveStartup(input({ hasInitialSession: true }), p);
+1 -1
View File
@@ -6,7 +6,7 @@ import { readSnipContext } from "./lib/snip";
* Entrypoint for the "chat" command. * Entrypoint for the "chat" command.
* *
* Deliberately thin: everything lives in {@link ChatView} so the same view can * Deliberately thin: everything lives in {@link ChatView} so the same view can
* also be pushed onto the navigation stack from the Chat History command. * also be pushed onto the navigation stack from the sessions command.
* *
* Assign this command a global shortcut in Vicinae's settings to get the * Assign this command a global shortcut in Vicinae's settings to get the
* launcher-and-start-typing behaviour. `fallbackText` is populated when the * launcher-and-start-typing behaviour. `fallbackText` is populated when the
+1 -1
View File
@@ -16,7 +16,7 @@ export const STORAGE_INDEX_KEY = "lam.sessions.index";
export const STORAGE_SESSION_PREFIX = "lam.session."; export const STORAGE_SESSION_PREFIX = "lam.session.";
/** /**
* The model picked in the Manage Models command. * The model picked in the Models command.
* *
* Separate from the `model` preference because an extension cannot write its own * Separate from the `model` preference because an extension cannot write its own
* preferences: Vicinae reads them once at startup and holds them in memory, so * preferences: Vicinae reads them once at startup and holds them in memory, so
+1 -1
View File
@@ -36,7 +36,7 @@ const FLUSH_INTERVAL_MS = 60;
export interface UseChatOptions { export interface UseChatOptions {
preferences: ResolvedPreferences; preferences: ResolvedPreferences;
/** Restores an existing conversation, e.g. when reopening from the Chat History command. */ /** Restores an existing conversation, e.g. when reopening from the sessions command. */
initialSession?: Session; initialSession?: Session;
/** /**
* Whether the model can read images, for the system prompt. * Whether the model can read images, for the system prompt.
+1 -1
View File
@@ -5,7 +5,7 @@ import { loadPreferences, type ResolvedPreferences } from "../preferences";
* The preferences a view should use, or `undefined` until they have loaded. * The preferences a view should use, or `undefined` until they have loaded.
* *
* Every reader goes through `loadPreferences`, which layers the model chosen in * Every reader goes through `loadPreferences`, which layers the model chosen in
* the Manage Models command on top of what Vicinae holds, and that read is asynchronous * the Models command on top of what Vicinae holds, and that read is asynchronous
* because the selection lives in LocalStorage. A hook rather than a synchronous * because the selection lives in LocalStorage. A hook rather than a synchronous
* call so that the one reader is also the only way to get preferences into a * call so that the one reader is also the only way to get preferences into a
* component, and a view that forgets to handle the loading case fails to compile * component, and a view that forgets to handle the loading case fails to compile
+3 -3
View File
@@ -39,7 +39,7 @@ export interface ResolvedPreferences {
model: string; model: string;
/** /**
* Whether {@link ResolvedPreferences.model} came from the preference or from a * Whether {@link ResolvedPreferences.model} came from the preference or from a
* pick in the Manage Models command. * pick in the Models command.
* *
* Carried rather than inferred, because the two disagree silently otherwise: * Carried rather than inferred, because the two disagree silently otherwise:
* the setup screen would show one model while the chat used another, and the * the setup screen would show one model while the chat used another, and the
@@ -244,14 +244,14 @@ export function resolvePreferences(raw: LamPreferences): ResolvedPreferences {
/** /**
* Vicinae's own preferences, resolved and normalised. Not exported: everything * Vicinae's own preferences, resolved and normalised. Not exported: everything
* outside this module should go through {@link loadPreferences}, which also * outside this module should go through {@link loadPreferences}, which also
* applies the model chosen in the Manage Models command. * applies the model chosen in the Models command.
*/ */
function getLamPreferences(): ResolvedPreferences { function getLamPreferences(): ResolvedPreferences {
return resolvePreferences(getPreferenceValues<LamPreferences>()); return resolvePreferences(getPreferenceValues<LamPreferences>());
} }
/** /**
* The model chosen in the Manage Models command, if there is one. * The model chosen in the Models command, if there is one.
* *
* Never throws. Storage that cannot be read means no selection rather than a * Never throws. Storage that cannot be read means no selection rather than a
* broken command, because the fallback (the preference) is always a valid * broken command, because the fallback (the preference) is always a valid
+1 -1
View File
@@ -18,7 +18,7 @@ import { errorMessage } from "./utils";
*/ */
export interface StartupInput { export interface StartupInput {
/** A conversation already in hand, which is how the Chat History command pushes this view. */ /** A conversation already in hand, which is how the sessions command pushes this view. */
hasInitialSession: boolean; hasInitialSession: boolean;
/** A conversation to reopen by id, handed over by the snip command. */ /** A conversation to reopen by id, handed over by the snip command. */
initialSessionId?: string; initialSessionId?: string;
+1 -1
View File
@@ -66,7 +66,7 @@ export async function visionSupport(check: VisionCheck): Promise<VisionSupport>
* Drops every cached answer. * Drops every cached answer.
* *
* A model can gain vision without its name changing (a re-pull, a different * A model can gain vision without its name changing (a re-pull, a different
* tag on the same account), so `Setup & Status` calls this rather than * tag on the same account), so `Set Up Lam Agent` calls this rather than
* leaving the user pinned to a stale "no". * leaving the user pinned to a stale "no".
*/ */
export async function forgetVisionSupport(): Promise<void> { export async function forgetVisionSupport(): Promise<void> {
+2 -2
View File
@@ -67,7 +67,7 @@ export default function ModelsCommand() {
// marked up relative to it: which row is current, and whether picking one // marked up relative to it: which row is current, and whether picking one
// would change anything. // would change anything.
if (!preferences) { if (!preferences) {
return <List isLoading navigationTitle="Manage Models" searchBarPlaceholder="Loading models…" />; return <List isLoading navigationTitle="Ollama Models" searchBarPlaceholder="Loading models…" />;
} }
return <ModelsScreen preferences={preferences} reloadPreferences={reload} />; return <ModelsScreen preferences={preferences} reloadPreferences={reload} />;
@@ -256,7 +256,7 @@ function ModelsScreen({ preferences, reloadPreferences }: ModelsScreenProps) {
return ( return (
<List <List
isLoading={isLoading} isLoading={isLoading}
navigationTitle="Manage Models" navigationTitle="Ollama Models"
searchBarPlaceholder="Search models…" searchBarPlaceholder="Search models…"
actions={listActions} actions={listActions}
> >
+1 -1
View File
@@ -78,7 +78,7 @@ export default function SessionsCommand() {
return ( return (
<List <List
isLoading={!loaded} isLoading={!loaded}
navigationTitle="Chat History" navigationTitle="Chat Sessions"
searchBarPlaceholder="Search conversations…" searchBarPlaceholder="Search conversations…"
> >
{loaded && items.length === 0 ? ( {loaded && items.length === 0 ? (
+3 -3
View File
@@ -56,11 +56,11 @@ export default function SetupCommand() {
// The screen does nothing until the model is known, because half of what it // The screen does nothing until the model is known, because half of what it
// reports is about that model: whether it can see images, and whether it is the // reports is about that model: whether it can see images, and whether it is the
// one from preferences or from the Manage Models command. Rendering it with the // one from preferences or from the Models command. Rendering it with the
// preference value and correcting it a moment later would flash a wrong answer // preference value and correcting it a moment later would flash a wrong answer
// at the user. // at the user.
if (!preferences) { if (!preferences) {
return <List isLoading navigationTitle="Setup & Status" searchBarPlaceholder="Loading preferences…" />; return <List isLoading navigationTitle="Lam Agent Setup" searchBarPlaceholder="Loading preferences…" />;
} }
return <SetupScreen preferences={preferences} reloadPreferences={reload} />; return <SetupScreen preferences={preferences} reloadPreferences={reload} />;
@@ -207,7 +207,7 @@ function SetupScreen({ preferences, reloadPreferences }: SetupScreenProps) {
return ( return (
<List <List
isLoading={isLoading} isLoading={isLoading}
navigationTitle="Setup & Status" navigationTitle="Lam Agent Setup"
searchBarPlaceholder="Search models…" searchBarPlaceholder="Search models…"
actions={settingsActions} actions={settingsActions}
> >
+2 -2
View File
@@ -34,7 +34,7 @@ function settleWindow(): Promise<void> {
* Bind it to a global shortcut for one-key "snip and ask". * Bind it to a global shortcut for one-key "snip and ask".
*/ */
export default async function SnipCommand(props: LaunchProps) { export default async function SnipCommand(props: LaunchProps) {
// Awaited because the model can come from a pick in the Manage Models command, which // Awaited because the model can come from a pick in the Models command, which
// lives in storage. The vision check below is about whichever model is in use, // lives in storage. The vision check below is about whichever model is in use,
// so it has to be the resolved one. // so it has to be the resolved one.
const preferences = await loadPreferences(); const preferences = await loadPreferences();
@@ -59,7 +59,7 @@ export default async function SnipCommand(props: LaunchProps) {
await showHUD( await showHUD(
vision === "no" vision === "no"
? `${preferences.model} cannot see images. Pick a vision model in preferences.` ? `${preferences.model} cannot see images. Pick a vision model in preferences.`
: `Could not check whether ${preferences.model} can see images. Run Setup & Status.`, : `Could not check whether ${preferences.model} can see images. Run Set Up Lam Agent.`,
); );
return; return;
} }