Skip to content
Open
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
2 changes: 1 addition & 1 deletion docs/docs/configure/skills.md
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,7 @@ altimate-code workspace review status # changes nothing on the server; --j

Requests sent from the CLI are recorded as such in the request's history. What the team gets is the exact version that was approved: editing the skill afterwards changes nothing for them until you run `skill publish` again, which submits the new version as an update. A member publishing to a workspace whose owner has review off is refused before anything is uploaded. If the skill uploads but cannot be sent (offline, an expired sign-in, too many open requests), the command says so and exits non-zero; running it again updates the uploaded skill and submits it. Against a workspace service without review, `skill publish` attaches the skill directly as before, which only the workspace owner can do. With `--json`, the output carries `ok`, the upload (`action`, `public_id`, `files`, `bytes`, `workspace_id`, `workspace_name`) and `review` with the service's `status`, `attached_directly`, `duplicate`, `owner_name` and `contribution` (`null` without review); every failure exits non-zero with `{"ok": false, "error": ...}`.

When the owner or an admin approves or rejects one of your requests that this client has seen pending (submitted from it, or listed by an earlier check), the TUI shows a notice once in each project linked to that workspace (with the reason for a rejection). Each altimate-code process with a linked project open checks at start and every 5 minutes (a server without review is asked again only after 6 hours) and keeps the 50 most recent decisions in your own state directory, so a decision that arrives while the TUI is closed or disconnected is shown when it next starts (as rule review notices are). The first time a TUI opens a project, decisions already kept are recorded without a notice; `workspace review status` lists them. `altimate-code workspace review status` lists your requests on the linked workspace (skills and knowledge bases): kind, name, status, whether it is new or an update, the reviewer and their reason, and `expired` for a request waiting longer than 14 days (it can still be approved).
When the owner or an admin approves or rejects one of your requests that this client has seen pending (submitted from it, or listed by an earlier check), the TUI shows a notice once in each project linked to that workspace (with the reason for a rejection). Each altimate-code process with a linked project open checks at start and every 5 minutes (a server without review is asked again only after 6 hours) and keeps the 50 most recent decisions in your own state directory, so a decision that arrives while the TUI is closed or disconnected is shown when it next starts (as rule review notices are). The first time a TUI opens a project, decisions already kept are recorded without a notice; `workspace review status` lists them. `altimate-code workspace review status` lists your requests on the linked workspace (skills and knowledge bases): kind, name, status, whether it is new or an update, the reviewer and their reason, and `expired` for a request waiting longer than 14 days (it can still be approved). In the web app you can withdraw a request that is still waiting, and remove an approved skill or knowledge base of yours from the workspace; the owner or an admin can remove it too, with a reason. When the owner or an admin removes one of yours that this client has seen approved (or waiting), the TUI shows that once too, with the reason; your own removal and withdrawal show nothing. `workspace review status` lists them as `withdrawn` and `removed from the workspace`.

`skill publish` sends the whole skill directory, not just `SKILL.md`, so keep secrets out of it. A built-in filter skips known file and directory names — `.env*`, `.git`, `id_rsa`, `*.pem`, `*.key`, `*.p12`, `.npmrc`/`.netrc`, `credentials.json`, `secrets.*`, `.ssh`/`.aws`, editor swap files — but it matches names only and never scans file contents, so a token inside `config.yaml` or a key named `server.crt` would still be uploaded. Built-in skills, global skills and skills the workspace itself sent you cannot be published — including the copies the VS Code / Cursor extension delivers into `.claude/skills/` and `.agents/skills/`, which carry an `.altimate-managed.json` marker.

Expand Down
2 changes: 1 addition & 1 deletion docs/docs/usage/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ With workspaces on:
- `/integrations` in the TUI opens the workspace's integration status directly (the same view as `/workspace` → Status). In a project that is not linked it offers the link, and when the workspace engine is missing or too old it offers the install and says which.
- In a project linked to a workspace, integrations come only from the workspace's engine, so the `datamate_manager` tool is not offered there and refuses to run. `datamate-*` MCP servers already in the config still load, and a one-time notice names the file they are in; the config is not edited.
- `altimate-code skill publish <name>` uploads a project skill to the linked workspace, attached directly for the workspace owner or an admin and sent for review for a member when review is on; see [Skills](../configure/skills.md#cli-commands).
- When the owner or an admin approves or rejects one of your skill or knowledge base requests that altimate-code has seen pending (submitted from it, or listed by an earlier check), the TUI shows a notice once in each project linked to that workspace, with the reason for a rejection. Every altimate-code process that opens a linked project checks at start and every 5 minutes while workspaces are on and you are signed in: it lists your own requests on the workspace (up to 10 pages of 50); a server without review is asked again only after 6 hours. The decision is kept per account and workspace in your own state directory (`$XDG_STATE_HOME/altimate-code/workspace-reviews/`, the 50 most recent), so a TUI that was closed or disconnected when it arrived shows it when it next starts. The first time a TUI opens a project, decisions already kept are recorded without a notice; `altimate-code workspace review status` lists them.
- When the owner or an admin approves or rejects one of your skill or knowledge base requests that altimate-code has seen pending (submitted from it, or listed by an earlier check), the TUI shows a notice once in each project linked to that workspace, with the reason for a rejection. Every altimate-code process that opens a linked project checks at start and every 5 minutes while workspaces are on and you are signed in: it lists your own requests on the workspace (up to 10 pages of 50); a server without review is asked again only after 6 hours. The decision is kept per account and workspace in your own state directory (`$XDG_STATE_HOME/altimate-code/workspace-reviews/`, the 50 most recent), so a TUI that was closed or disconnected when it arrived shows it when it next starts. The first time a TUI opens a project, decisions already kept are recorded without a notice; `altimate-code workspace review status` lists them. Withdrawing a waiting request and removing an approved one happen in the web app; when the owner or an admin removes one of yours from the workspace, the TUI shows that once too, with the reason.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: This wording promises a removal notice for any request, but the client only notices removals it previously saw as approved or waiting and stays silent for the user's own removal. State those conditions here so users do not expect notices for unseen or self-removed requests.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At docs/docs/usage/cli.md, line 70:

<comment>This wording promises a removal notice for any request, but the client only notices removals it previously saw as approved or waiting and stays silent for the user's own removal. State those conditions here so users do not expect notices for unseen or self-removed requests.</comment>

<file context>
@@ -67,7 +67,7 @@ With workspaces on:
 - In a project linked to a workspace, integrations come only from the workspace's engine, so the `datamate_manager` tool is not offered there and refuses to run. `datamate-*` MCP servers already in the config still load, and a one-time notice names the file they are in; the config is not edited.
 - `altimate-code skill publish <name>` uploads a project skill to the linked workspace, attached directly for the workspace owner or an admin and sent for review for a member when review is on; see [Skills](../configure/skills.md#cli-commands).
-- When the owner or an admin approves or rejects one of your skill or knowledge base requests that altimate-code has seen pending (submitted from it, or listed by an earlier check), the TUI shows a notice once in each project linked to that workspace, with the reason for a rejection. Every altimate-code process that opens a linked project checks at start and every 5 minutes while workspaces are on and you are signed in: it lists your own requests on the workspace (up to 10 pages of 50); a server without review is asked again only after 6 hours. The decision is kept per account and workspace in your own state directory (`$XDG_STATE_HOME/altimate-code/workspace-reviews/`, the 50 most recent), so a TUI that was closed or disconnected when it arrived shows it when it next starts. The first time a TUI opens a project, decisions already kept are recorded without a notice; `altimate-code workspace review status` lists them.
+- When the owner or an admin approves or rejects one of your skill or knowledge base requests that altimate-code has seen pending (submitted from it, or listed by an earlier check), the TUI shows a notice once in each project linked to that workspace, with the reason for a rejection. Every altimate-code process that opens a linked project checks at start and every 5 minutes while workspaces are on and you are signed in: it lists your own requests on the workspace (up to 10 pages of 50); a server without review is asked again only after 6 hours. The decision is kept per account and workspace in your own state directory (`$XDG_STATE_HOME/altimate-code/workspace-reviews/`, the 50 most recent), so a TUI that was closed or disconnected when it arrived shows it when it next starts. The first time a TUI opens a project, decisions already kept are recorded without a notice; `altimate-code workspace review status` lists them. Withdrawing a waiting request and removing an approved one happen in the web app; when the owner or an admin removes one of yours from the workspace, the TUI shows that once too, with the reason.
 - Workspace skills are synced at the version the workspace serves you: the approved version, or your own pending version of a skill you submitted. The next sync follows what the workspace serves: a skill whose first submission is rejected, or that is detached from the workspace, is removed from the project; when an update to an approved skill is rejected or replaced, the skill stays at its approved version. Each skill is downloaded as one version: if it is approved or rejected while its files are being read, nothing of it is installed from that sync and it is fetched again on the next one. When a download fails, the copy already in the project is kept only if it is still the version the workspace serves, as far as the server has said (its list, or a read refused because the version just changed), or if neither the copy nor the list names a version (a copy synced before revisions, or an older server), in which case it is kept until the server says otherwise; a copy of a version it no longer serves (for example your rejected pending version), or of a skill it no longer lists, is removed until the download succeeds, even when no other skill could be downloaded. Two processes syncing the same project take turns: a sync never replaces or removes a snapshot another process installed after it started, and the next sync starts from the newer one.
 - When the linked workspace has knowledge, the agent is told to check it for best practices and team conventions before answering, without you having to mention the knowledge base.
</file context>
Suggested change
- When the owner or an admin approves or rejects one of your skill or knowledge base requests that altimate-code has seen pending (submitted from it, or listed by an earlier check), the TUI shows a notice once in each project linked to that workspace, with the reason for a rejection. Every altimate-code process that opens a linked project checks at start and every 5 minutes while workspaces are on and you are signed in: it lists your own requests on the workspace (up to 10 pages of 50); a server without review is asked again only after 6 hours. The decision is kept per account and workspace in your own state directory (`$XDG_STATE_HOME/altimate-code/workspace-reviews/`, the 50 most recent), so a TUI that was closed or disconnected when it arrived shows it when it next starts. The first time a TUI opens a project, decisions already kept are recorded without a notice; `altimate-code workspace review status` lists them. Withdrawing a waiting request and removing an approved one happen in the web app; when the owner or an admin removes one of yours from the workspace, the TUI shows that once too, with the reason.
- When the owner or an admin approves or rejects one of your skill or knowledge base requests that altimate-code has seen pending (submitted from it, or listed by an earlier check), the TUI shows a notice once in each project linked to that workspace, with the reason for a rejection. Every altimate-code process that opens a linked project checks at start and every 5 minutes while workspaces are on and you are signed in: it lists your own requests on the workspace (up to 10 pages of 50); a server without review is asked again only after 6 hours. The decision is kept per account and workspace in your own state directory (`$XDG_STATE_HOME/altimate-code/workspace-reviews/`, the 50 most recent), so a TUI that was closed or disconnected when it arrived shows it when it next starts. The first time a TUI opens a project, decisions already kept are recorded without a notice; `altimate-code workspace review status` lists them. Withdrawing a waiting request and removing an approved one happen in the web app; when the owner or an admin removes one of yours that this client has seen approved (or waiting), the TUI shows that once too, with the reason; your own removal and withdrawal are silent.

- Workspace skills are synced at the version the workspace serves you: the approved version, or your own pending version of a skill you submitted. The next sync follows what the workspace serves: a skill whose first submission is rejected, or that is detached from the workspace, is removed from the project; when an update to an approved skill is rejected or replaced, the skill stays at its approved version. Each skill is downloaded as one version: if it is approved or rejected while its files are being read, nothing of it is installed from that sync and it is fetched again on the next one. When a download fails, the copy already in the project is kept only if it is still the version the workspace serves, as far as the server has said (its list, or a read refused because the version just changed), or if neither the copy nor the list names a version (a copy synced before revisions, or an older server), in which case it is kept until the server says otherwise; a copy of a version it no longer serves (for example your rejected pending version), or of a skill it no longer lists, is removed until the download succeeds, even when no other skill could be downloaded. Two processes syncing the same project take turns: a sync never replaces or removes a snapshot another process installed after it started, and the next sync starts from the newer one.
- When the linked workspace has knowledge, the agent is told to check it for best practices and team conventions before answering, without you having to mention the knowledge base.
- In an ordinary session the agent is told every turn which workspace the project is linked to — or that none is, or that the link could not be verified just now. In an extension-pinned session it is told the pinned workspace instead, and that it differs from the project's own link. Either way "which workspace am I in?" has an answer, and the agent is told not to confuse it with a Databricks workspace or an IDE workspace folder.
Expand Down
10 changes: 9 additions & 1 deletion packages/opencode/src/altimate/workspace/review-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,10 @@ export const Contribution = z.object({
name: z.string(),
description: z.string().nullable().optional(),
change: z.enum(["add", "edit"]),
status: z.enum(["pending", "active", "rejected", "superseded"]),
// `withdrawn`: the author withdrew it (or it was withdrawn when the author deleted the item or lost access);
// `retired`: removed from the workspace after approval, by an owner, an admin or the author. A status this version
// does not know makes the row unreadable (skipped and counted), never misread.
status: z.enum(["pending", "active", "rejected", "superseded", "withdrawn", "retired"]),
version: z.number().int(),
is_mine: z.boolean(),
proposed_by_name: z.string().nullable().optional(),
Expand All @@ -53,6 +56,11 @@ export const Contribution = z.object({
expired: z.boolean().default(false),
file_count: z.number().int().nullable().optional(),
document_version: z.number().int().nullable().optional(),
/** Whether the caller made the last decision (`reviewed_by`): a person is not told of their own retire. Absent from
* older servers: read as not. */
decided_by_me: z.boolean().default(false),
/** For owners and admins, on open requests: false when the author can no longer see the workspace. */
author_has_access: z.boolean().nullable().optional(),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SUGGESTION: author_has_access is parsed but never read

Nothing in the client consumes this field (rg author_has_access matches only this declaration), so it is dead wire surface, even though the PR description says it is read. Either drop it or use it (for example in the notice or workspace review status wording for an author who lost access).

})
export type Contribution = z.infer<typeof Contribution>

Expand Down
45 changes: 35 additions & 10 deletions packages/opencode/src/altimate/workspace/review-notices.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@
// (`review_notices`) when it next starts or refreshes. The TUI remembers per project which notices it showed.
//
// - What is noticed: a submission this client has seen pending (from `skill publish`, or an earlier poll) that the
// server now lists as approved (`active`) or `rejected`. A superseded one (replaced by the author's newer submission)
// is let go silently. Decisions on requests this client never saw pending are history, not news.
// server now lists as approved (`active`) or `rejected`; and one this client has seen approved (or pending) that an
// owner or admin then removed from the workspace (`retired`, by someone else: the person's own removal is not news).
// A superseded one (replaced by the author's newer submission) or a withdrawn one is let go silently. Decisions on
// requests this client never saw pending are history, not news.
// - State: in the person's own state directory, outside every repository (`<state>/workspace-reviews/<digest>.json`), one
// file per API URL, tenant, account (the person, `user:<id>`) and workspace. Another account or workspace reads
// another file, so a switch, relink or sign-in as someone else never shows or records this one's notices. Bounded:
Expand Down Expand Up @@ -41,6 +43,8 @@ export function resetForTest(): void {
unsupportedAt.clear()
}
export const MAX_SEEN = 500
/** Approved requests tracked, so their removal from the workspace is noticed. */
export const MAX_APPROVED = 200
/** Notices kept for a client to replay, newest last: as many as the rules state keeps. */
export const MAX_NOTICES = 50
const LOCK = { staleMs: 60_000, timeoutMs: 10_000 }
Expand All @@ -58,7 +62,8 @@ export const Notice = z.object({
public_id: z.string(),
kind: z.enum(["skill", "knowledge"]),
name: z.string(),
status: z.enum(["active", "rejected"]),
/** `retired`: removed from the workspace after approval. */
status: z.enum(["active", "rejected", "retired"]),
reason: z.string().optional(),
reviewer: z.string().optional(),
/** When it was decided (the server's `reviewed_at`, else `updated_at`). */
Expand All @@ -71,6 +76,8 @@ export const State = z.object({
scope: Scope,
/** Contribution ids this client has seen pending. */
pending: z.array(z.string()).default([]),
/** Contribution ids this client has seen approved (`active`), whose removal is noticed. Absent from older files. */
approved: z.array(z.string()).default([]),
/** `<public_id>:<status>` of decisions already recorded as notices. */
seen: z.array(z.string()).default([]),
/** The recorded notices, kept for clients to show (replayed from the rules summary). */
Expand Down Expand Up @@ -122,7 +129,7 @@ async function locked<T>(scope: Scope, change: (state: State) => Promise<{ state
return Flock.withLock(
`workspace-reviews:${path.basename(stateFile(scope))}`,
async () => {
const current = (await read(scope)) ?? { version: 1 as const, scope, pending: [], seen: [], notices: [] }
const current = (await read(scope)) ?? { version: 1 as const, scope, pending: [], approved: [], seen: [], notices: [] }
const { state, result } = await change(current)
const next = cap(state)
if (JSON.stringify(next) !== JSON.stringify(current)) await write(next)
Expand All @@ -136,6 +143,7 @@ function cap(state: State): State {
return {
...state,
pending: state.pending.slice(-MAX_PENDING),
approved: state.approved.slice(-MAX_APPROVED),
seen: state.seen.slice(-MAX_SEEN),
notices: state.notices.slice(-MAX_NOTICES),
}
Expand All @@ -146,10 +154,15 @@ const keyOf = (id: string, status: string) => `${id}:${status}`
/** The decisions in `items` this client has not recorded yet: the caller's own, seen pending before. Pure. */

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SUGGESTION: The docstring no longer describes decided

"seen pending before" omits the new case: a retired row now also yields a notice for a request this client saw approved. The inline comment below states it; the summary line still says pending only.

Suggested change
/** The decisions in `items` this client has not recorded yet: the caller's own, seen pending before. Pure. */
/** The decisions in `items` this client has not recorded yet: the caller's own, seen pending or approved before. Pure. */

export function decided(state: State, items: readonly ReviewApi.Contribution[]): Notice[] {
const pending = new Set(state.pending)
const approved = new Set(state.approved)
const seen = new Set(state.seen)
return items.flatMap((item): Notice[] => {
if (!item.is_mine || (item.status !== "active" && item.status !== "rejected")) return []
if (!pending.has(item.public_id) || seen.has(keyOf(item.public_id, item.status))) return []
if (!item.is_mine || seen.has(keyOf(item.public_id, item.status))) return []
if (item.status === "retired") {
// Removed by someone else from a workspace this client saw it in (approved, or still pending before that).
if (item.decided_by_me || (!approved.has(item.public_id) && !pending.has(item.public_id))) return []
} else if (item.status !== "active" && item.status !== "rejected") return []
else if (!pending.has(item.public_id)) return []
return [
{
public_id: item.public_id,
Expand All @@ -165,22 +178,34 @@ export function decided(state: State, items: readonly ReviewApi.Contribution[]):
}

/** The state after `items` was listed and `shown` notices were recorded (kept for replay, and marked seen). A decision
* not recorded stays pending; a superseded request is let go. With the whole list (`complete`), ids it no longer holds
* are let go. */
* not recorded stays pending; a superseded or withdrawn request is let go. Approved ones are tracked until they are
* removed. With the whole list (`complete`), ids it no longer holds are let go. */
export function advance(state: State, items: readonly ReviewApi.Contribution[], shown: readonly Notice[], complete: boolean): State {
const mine = items.filter((item) => item.is_mine)
const listed = new Set(mine.map((item) => item.public_id))
const shownIds = new Set(shown.map((notice) => notice.public_id))
const settled = new Set(
mine
.filter((item) => item.status === "superseded" || ((item.status === "active" || item.status === "rejected") && (shownIds.has(item.public_id) || !state.pending.includes(item.public_id))))
.filter(
(item) =>
item.status === "superseded" ||
item.status === "withdrawn" ||
item.status === "retired" ||
((item.status === "active" || item.status === "rejected") && (shownIds.has(item.public_id) || !state.pending.includes(item.public_id))),
)
.map((item) => item.public_id),
)
const pending = [...new Set([...state.pending, ...mine.filter((item) => item.status === "pending").map((item) => item.public_id)])].filter(
(id) => !settled.has(id) && (!complete || listed.has(id)),
)
// Approved ones are tracked until anything else (retired, or gone from a complete list) settles them. An approved one
// still pending here is kept pending too, until its approval is recorded.
const ended = new Set(mine.filter((item) => item.status !== "active").map((item) => item.public_id))
const approved = [...new Set([...state.approved, ...mine.filter((item) => item.status === "active").map((item) => item.public_id)])].filter(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Approved IDs beyond the view=mine page cap are never fetched by ID, so an older approved request can be retired without producing the promised removal notice. Include tracked approved IDs in the capped lookup queue alongside pending IDs.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At packages/opencode/src/altimate/workspace/review-notices.ts, line 204:

<comment>Approved IDs beyond the `view=mine` page cap are never fetched by ID, so an older approved request can be retired without producing the promised removal notice. Include tracked approved IDs in the capped lookup queue alongside pending IDs.</comment>

<file context>
@@ -165,22 +178,34 @@ export function decided(state: State, items: readonly ReviewApi.Contribution[]):
+  // Approved ones are tracked until anything else (retired, or gone from a complete list) settles them. An approved one
+  // still pending here is kept pending too, until its approval is recorded.
+  const ended = new Set(mine.filter((item) => item.status !== "active").map((item) => item.public_id))
+  const approved = [...new Set([...state.approved, ...mine.filter((item) => item.status === "active").map((item) => item.public_id)])].filter(
+    (id) => !ended.has(id) && (!complete || listed.has(id)),
+  )
</file context>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: approved tracking rotates once it exceeds MAX_APPROVED, so removals can be silently missed

With more than 200 approved rows in the list the tracked set is not stable: state.approved is seeded first, the current list (newest first) is appended, and cap() keeps the tail (slice(-MAX_APPROVED)). At 400 active rows the two halves swap on every poll, so roughly half the approved requests are untracked at any moment; at 201 rows a different id drops out each poll (which also rewrites the state file every poll). A retired row whose id is untracked produces no notice, because decided() requires approved.has(id) || pending.has(id).

Keep the cap deterministic (build the id list in list order and keep the newest MAX_APPROVED before capping), and add a test that exceeds MAX_APPROVED.

(id) => !ended.has(id) && (!complete || listed.has(id)),
)
const seen = [...new Set([...state.seen, ...shown.map((notice) => keyOf(notice.public_id, notice.status))])]
return { ...state, pending, seen, notices: [...state.notices, ...shown] }
return { ...state, pending, approved, seen, notices: [...state.notices, ...shown] }
}

/** Remember a request this client just submitted as pending, so its decision is noticed even before the next poll lists
Expand Down
4 changes: 3 additions & 1 deletion packages/opencode/src/cli/cmd/workspace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,8 @@ const REVIEW_STATUS: Record<ReviewApi.Contribution["status"], string> = {
active: "approved",
rejected: "rejected",
superseded: "replaced by a newer request",
withdrawn: "withdrawn",
retired: "removed from the workspace",
}

/** One line per request for `workspace review status`. Pure, so the wording is testable. Server text is made printable. */
Expand All @@ -350,7 +352,7 @@ export function describeReviews(workspaceName: string, listed: Awaited<ReturnTyp
const change = item.change === "edit" ? "update" : "new"
const reviewer = item.reviewed_by_name ? ` by ${printable(item.reviewed_by_name)}` : ""
const reason = item.review_reason ? `: ${printable(item.review_reason)}` : ""
const decided = item.status === "active" || item.status === "rejected" ? `${reviewer}${reason}` : ""
const decided = item.status === "active" || item.status === "rejected" || item.status === "retired" ? `${reviewer}${reason}` : ""
// Expired requests stay pending and can still be approved: the label says they have waited past the window.
const expired = item.status === "pending" && item.expired ? " (expired: waiting longer than 14 days)" : ""
lines.push(` ${item.kind} "${printable(item.name)}" (${change}): ${REVIEW_STATUS[item.status]}${decided}${expired}`)
Expand Down
1 change: 1 addition & 0 deletions packages/opencode/src/plugin/tui/altimate/rules.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -507,6 +507,7 @@ function reviewToast(api: TuiPluginApi, raw: { text: string; status: string; rea
const what = p.kind === "skill" ? `Your skill ${quoted}` : `Your knowledge base ${quoted}`
if (p.status === "active") api.ui.toast({ variant: "success", message: `${what} was approved${who}. It now reaches the workspace.`, duration: 10000 })
else if (p.status === "rejected") api.ui.toast({ variant: "warning", message: `${what} was rejected${who}${p.reason ? ` — ${p.reason}` : ""}`, duration: 15000 })
else if (p.status === "retired") api.ui.toast({ variant: "warning", message: `${what} was removed from the workspace${who}${p.reason ? ` — ${p.reason}` : ""}`, duration: 15000 })
// A status this version does not know (a newer server) is still shown, not dropped.
else api.ui.toast({ variant: "info", message: `${what} is now ${printable(p.status)}${who}.`, duration: 10000 })
return
Expand Down
Loading
Loading