Skip to content

Leafdesk 0.4.0

Choose a tag to compare

@erhanerbas erhanerbas released this 02 Oct 20:33

Upgrading from 0.3.0

  • Migrations (0031–0035) run automatically when the container starts.
  • OpenCode Go: to use it with Docker Compose, download the new docker-compose.yml, which
    passes OPENCODE_API_KEY to the app (or set the key as AI_API_KEY).

Added

  • The AI chat can add and change things, as the person and only where they may edit:
    create_row adds a database row (values and text), update_row changes a row's values or
    title, create_page adds a page under a page (or a private one at the top of the workspace; in
    a page's scope, only under it). A mode under the question box, remembered in the browser, says
    what it may do: Ask (the default) puts each change in the box's place before it's made (what,
    where, the values, the start of the text) with Yes, Yes, and don't ask again (switches to
    Auto) and No (1, 2, 3; Escape says no); Auto doesn't ask; Read only doesn't offer the
    model changes. Changes are checked (target, edit access, values) before the person is asked; the
    answer's stream waits for the decision (decideChangeAction, only the asker's), pinging every
    20 seconds, and stopping, leaving or 15 minutes without a decision write nothing. Changes made,
    declined or failed are steps with links, kept with the conversation even when no answer text
    came, and noted with their ids for later questions. The API takes mode (ask, auto,
    read). Popover takes side="top". The chat route no longer writes to a stream the browser
    has closed. New checks in scripts/ai-chat-e2e.ts (120 in all) and src/server/ai/chat.test.ts.
  • Audit log (#61), in Settings → Audit log for owners (members, guests and everyone else get a
    404): who changed what in the workspace, newest first, 50 to a page, filtered by person (or the
    identity provider, or the server), kind of change and a range of days in the viewer's time zone,
    and downloadable as CSV with the same filters (up to 10,000 events). Recorded: members added,
    joining (join link, email domain, single sign-on, SCIM), removed or leaving, role changes and
    handing over ownership; invitations sent, revoked and accepted, the join link turned on, off or
    replaced, join requests approved or declined; page sharing with a person, everyone or a group
    (and by email), and access requests decided; teamspaces created, changed, archived, restored and
    their members, roles and groups; groups created, renamed, deleted and their members; the
    workspace renamed and its settings, with the before and after of each changed setting; single
    sign-on saved, verified or removed, SCIM tokens created or revoked; pages deleted for good (by a
    person, or by the trash cleanup as the system), published, unpublished or taken off the web by an
    owner, the site saved or removed; API tokens created or revoked and apps connected or
    disconnected (in each workspace they reach); exports (the workspace ZIP, a page as ZIP, Markdown,
    CSV or the print view). Each event names the person, the API token or connected app they acted
    through, or the SCIM token, with the request's address and browser, and reads as a sentence in
    the viewer's language. Recording never fails or holds back a change: it runs in the change's
    transaction (a savepoint) where there is one, right after it otherwise, and a failure is only
    logged. The daily retention cleanup prunes events older than a year. Migration 0031_audit_log
    (audit_event). New checks: scripts/audit-e2e.ts (169), src/lib/audit.test.ts.
  • Reorder columns by dragging. A table's column headers, and the properties in any view's
    properties menu, can be dragged into a new order, which the view keeps (propertyOrder in its
    settings, so each view has its own order and the database's property order stays as it is).
    Boards, galleries, lists, timelines and published pages show properties in that order too.
    Hidden columns keep their place, properties added later show at the end, and a
    view saved by someone who can't see some columns leaves those where they were.
  • Resize table columns by dragging. Each column header, the Name column's too, has a handle on
    its right edge; the column follows the pointer and keeps the width it is let go at, which the
    view saves (columnWidths in its settings, per view). Phones keep the narrow Name column.
  • Customize the sidebar per workspace. "Customize sidebar" at the bottom of the sidebar lists
    its sections (Favorites, Teamspaces, Shared, Private) to reorder by dragging or with the arrows
    and to hide or show. The order, hidden sections and which headings are folded are kept for each
    person in each workspace, on every device, so a personal workspace and a company one can look
    different; folding a heading no longer changes it in every workspace. Migration
    0035_member_sidebar (workspace_member.sidebar).
  • Property access. Someone with full access to a database opens Property access from a
    column's menu and sets what everyone with access to the database may do with that property: edit
    it, edit its values, view them, see only the column, or nothing, with exceptions for people,
    groups and whoever a person or created-by property of the row names. The widest rule wins, nobody
    gets more than their database access, and full access is never restricted; the dialog says who
    still has full access and links to the database's Share panel. Relations and system properties
    can't be restricted. Values a person may not see are left out before formulas run (formulas and
    rollups over them are dropped), and views, filters, sorts, search, the semantic index, autofill,
    exports, print, published pages, MCP (new set_property_access tool) and the REST API never
    reveal them; writes, board drags, bulk edits, schema changes, CSV imports, forms, templates and
    copies follow the same rules. Assignment notices and emails skip people who can't see the person
    property. Changes land in the audit log. Migration 0033_property_access
    (property_permission).
  • What a teamspace's members get on its pages. A teamspace's owners can lower the access its
    members get where a page doesn't decide (full, edit, comment or view). Full stays the default, so
    nothing changes for existing workspaces. The teamspace's owners and the workspace's owners keep
    full access, and whoever adds or moves a page to the top of a lowered teamspace gets full access
    to it. Migration 0034_teamspace_member_level.
  • OpenCode Go as an AI provider: AI_PROVIDER=opencode-go with a Go key (AI_API_KEY or
    OPENCODE_API_KEY) and any model of its catalog, e.g. AI_MODEL=kimi-k3. Each model goes to the
    API it is served over (chat completions, Responses or Anthropic's Messages) with its own context
    window. Requests carry the x-opencode-session header Go asks for (the conversation's id in the
    AI chat, a new one for every other request) and name Leafdesk as their user agent; other
    providers get neither. New checks in src/server/ai/providers.test.ts.

Changed

  • Ask AI sits beside Home in the sidebar, as a ✨ icon next to People, where it took a row of
    its own.
  • Actions under AI chat messages. An answer has Copy (its Markdown, without the citation
    numbers), Answer again (the latest one) and when it was written; a question shows, on hover,
    when it was asked, Edit (the latest one: the edited question is answered in place of it) and
    Copy. Answering again or an edited question replaces the last question and answer once the new
    answer is kept (replaceLast in the API); one that fails leaves them as they were. An answer
    that changed things can't be answered again or edited (it would make the changes twice).
    Messages now carry their time (at). New checks in scripts/ai-chat-e2e.ts (126 in all).
  • Escape closes one thing at a time. In the AI chat panel, Escape in a menu (the mode, the
    history) or while editing a question closed the panel too; now it only closes the menu or the
    edit. Menus (useDismiss) mark the Escape they handle, for whatever else listens.
  • AI chat answers no longer list their sources below. The numbered citations in the text stay:
    they open the page (at the passage) and name it on hover.
  • The AI chat decides what to look at. The question was searched for before the model saw
    it, as a whole ("Bekleyen ne iş var" found nothing, and the model then guessed words to search
    for). Now nothing is searched for it: the question goes with a map of what the person can open in
    the workspace (or under the scope page): the databases with their properties, types and options
    (a status's options by group: to do, in progress, done), and the pages, each with its id and
    where it is, up to 8,000 characters. The model is told to think first: to query a database for
    tasks and records, to search with a few distinctive words rather than the question, and to
    answer greetings without tools; and to write everything, also what it says before using a tool,
    in the language of the question. Six turns per question, where there were five. The full-page
    chat lists a new conversation in the sidebar as soon as it starts, not once it's answered. New
    checks in scripts/ai-chat-e2e.ts and src/server/ai/chat.test.ts.
  • The AI chat can query databases. A new query_database tool lists the rows of a database that
    match filters on its properties (the same filters, operators, groups and sorts as the MCP tool:
    "assigned to me", "status is open", "due in the next 30 days"…), run as the person through the
    same operation, so values they may not see can't be filtered on or returned. Each row found is a
    source the answer can cite. It is refused for databases the person can't open, outside the
    chosen scope or in another workspace, and an invalid filter comes back to the model saying why.
    read_page on a database now lists its properties with their types and options, and a row found
    by search names its database, so the model knows what to filter on. The query shows as a step
    ("Queried Tasks · Status = Open or Due within next 30 days · 12 rows", in the words of the
    database's filter menu), kept with the conversation. Databases in the chat's steps and sources
    show the database icon when they have no icon of their own, as in the sidebar. New checks in
    scripts/ai-chat-e2e.ts.
  • The AI chat shows the steps it takes. Above each answer, while it's written: Working · 4s
    with the steps listed as they happen (the search with the question and how many pages it found,
    what the model says before searching or reading, its searches with their result counts, and the
    pages it reads as links), then folded into Worked for 16s, which opens them again. Steps and
    time are kept with the conversation (answers from before have none); a page read that the person
    can no longer open is shown without its title. Chat answers carry step events in place of
    tool, and done says how long the answer took. New checks in scripts/ai-chat-e2e.ts.
  • The AI chat has a page of its own, and the panel opens from the page. Ask AI in the
    sidebar now opens the chat across the page (/w/[id]/ai, ?c= naming the conversation), where
    the sidebar lists the conversations in place of the pages: by when they were last added to
    (today, yesterday, the last 7 and 30 days, older), each one a link, with New chat, deleting one
    or all, and Back to pages to the page you came from. The panel beside a page opens from a ✨
    button in the page's header and can be expanded to the full page with its conversation. A source
    clicked in the full page opens its page with the conversation carried on in the panel (on a
    phone, just the page). New checks in src/lib/ai-chat.test.ts.
  • The AI chat finds pages from whole questions, and reads databases' rows. Its full-text search
    required every word of the question on a page, so "Kart ekstreleri ne durumda?" found nothing
    without an embeddings model. It now finds pages with any of the words, as prefixes (long words
    cut to five letters, a rough stem for suffixes), without question words and fillers, titles
    ranked first; the search box, MCP and the REST API still want all the words. read_page on a
    database lists its first 200 rows with the values the person may see, where it gave only the
    property names, and a row a search finds comes with its values (those the person may see), as
    its page's text has none of them. New checks in scripts/ai-chat-e2e.ts and
    src/lib/search-words.test.ts.
  • AI chat answers are shown as markdown (GitHub's dialect, through react-markdown and
    remark-gfm): tables, italics, strikethrough, links (opened in a new tab), code blocks, quotes,
    task lists and nested lists, where only bullets, headings, bold and inline code were recognised
    before. Citations stay buttons to their sources, also inside tables; code is left as written.
    Images are not loaded and raw HTML shows as text, so an answer repeating a page can't make the
    browser fetch an address or run markup. The renderer loads with the panel's first answer.
  • The AI chat says it is thinking while the model works, before each of its turns, instead of
    showing the question as a search ("Searching for “Hello”…" until the first word arrived). The
    model's own searches and the pages it reads are still shown as they happen. Chat answers carry a
    new thinking event.
  • Emails in the recipient's language. Every email to someone with an account (shares,
    comments, mentions, reminders, assignments, access and join requests and their answers, password
    resets, verification and account notices) is written in the language they last used: the one
    they picked in My account → Language, or their browser's, stored with their account
    (user_preference.locale) when they sign in or up and when they change it, not on every request.
    Until it is known it falls back to the language the email was queued with, as before (the
    actor's, or the requester's); invitations to addresses without an account still go out in the
    inviter's. At sign-in a language picked on that device replaces the stored one, while a browser's
    only fills it in when none is stored, so signing in elsewhere keeps the language someone chose; a
    sign-in that states no language (no cookie, no Accept-Language) keeps it too.
  • Settings → Guests shows who really invited a guest. The membership now records who brought
    each person in (workspace_member.invited_by): whoever added them or shared a page with them,
    sent the invitation they redeemed (also through the join link), or approved their request.
    Nobody for those who came in on their own (join link, allowed email domain, single sign-on,
    SCIM). The list used to guess from the oldest page entry someone else gave them, and still does
    where nothing is recorded. The migration fills it in for existing members from the audit log
    (added by someone else, or the sender of the invitation they accepted), and for other guests
    with the old guess. Migration 0032_recipient_locale_and_inviter. New checks:
    src/server/mail/locale.test.ts, and in scripts/access-requests-e2e.ts, person-e2e.ts,
    account-e2e.ts, guests-e2e.ts and membership-e2e.ts.